main_ui.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. #!/usr/bin/python
  2. ''' Hypervideo GUI '''
  3. import sys
  4. from PyQt5.QtCore import (
  5. QFile,
  6. QPoint,
  7. QRect,
  8. QSize,
  9. QStandardPaths,
  10. Qt,
  11. QProcess,
  12. QSettings
  13. )
  14. from PyQt5.QtGui import QIcon, QFont
  15. from PyQt5.QtWidgets import (
  16. QAction,
  17. QApplication,
  18. QComboBox,
  19. QFileDialog,
  20. QHBoxLayout,
  21. QLineEdit,
  22. QLabel,
  23. QMainWindow,
  24. QMessageBox,
  25. QProgressBar,
  26. QPushButton,
  27. QToolButton,
  28. QVBoxLayout,
  29. QWidget,
  30. )
  31. # Debugging
  32. if len(sys.argv) == 2:
  33. if sys.argv[1] == '-v':
  34. DEBUG = 0
  35. else:
  36. DEBUG = None
  37. else:
  38. DEBUG = None
  39. __version__ = '1.0.2'
  40. __license__ = 'GPL-3'
  41. __title__ = 'Simple Hypervideo Download GUI'
  42. class MainWindow(QMainWindow):
  43. ''' MainWindow '''
  44. def __init__(self):
  45. ''' Initial '''
  46. super(MainWindow, self).__init__()
  47. self.hypervideo_bin = None
  48. self.url_catch = None
  49. self.out_folder_path = '/tmp/'
  50. self.settings = QSettings('YouTubeDL', 'YTDL')
  51. self.setAttribute(Qt.WA_DeleteOnClose)
  52. self.create_status_bar()
  53. pyfile = QStandardPaths.findExecutable("hypervideo")
  54. if not pyfile == "":
  55. debugging('Found executable: %s' % pyfile)
  56. self.hypervideo_bin = pyfile
  57. else:
  58. self.msgbox("hypervideo not found\nPlease install hypervideo")
  59. self.dflt_fms_menu_items = ['Video/Audio - Best Quality',
  60. 'Audio Only - Best Quality']
  61. self.list = []
  62. self.init_ui()
  63. def init_ui(self):
  64. ''' Initial UI '''
  65. self.setWindowTitle(__title__)
  66. btnwidth = 155
  67. self.cmd = None
  68. self.proc = QProcess(self)
  69. self.proc.started.connect(lambda: self.show_msg("Creating list"))
  70. self.proc.started.connect(lambda: self.fmts_btn.setEnabled(False))
  71. self.proc.finished.connect(lambda: self.show_msg("List done!"))
  72. self.proc.finished.connect(self.process_finished)
  73. self.proc.finished.connect(lambda: self.fmts_btn.setEnabled(True))
  74. self.proc.readyRead.connect(self.process_output)
  75. self.dl_proc = QProcess(self)
  76. self.dl_proc.setProcessChannelMode(QProcess.MergedChannels)
  77. self.dl_proc.started.connect(lambda: self.show_msg("Download started"))
  78. self.dl_proc.started.connect(lambda: self.dl_btn.setEnabled(False))
  79. self.dl_proc.started.connect(lambda: self.fmts_btn.setEnabled(False))
  80. self.dl_proc.started.connect(lambda: self.cxl_btn.setEnabled(True))
  81. self.dl_proc.finished.connect(self.msg_dl_finished)
  82. self.dl_proc.finished.connect(lambda: self.dl_btn.setEnabled(True))
  83. self.dl_proc.finished.connect(lambda: self.fmts_btn.setEnabled(True))
  84. self.dl_proc.finished.connect(lambda: self.cxl_btn.setEnabled(False))
  85. self.dl_proc.finished.connect(lambda: self.setWindowTitle(__title__))
  86. self.dl_proc.readyRead.connect(self.dl_process_out)
  87. self.setGeometry(0, 0, 600, 250)
  88. self.setFixedSize(600, 250)
  89. self.setStyleSheet(ui_style_sheet(self))
  90. # Menu
  91. main_menu = self.menuBar()
  92. file_menu = main_menu.addMenu('File')
  93. help_menu = main_menu.addMenu('Help')
  94. # Exit button
  95. exit_button = QAction('Exit', self)
  96. exit_button.setShortcut('Ctrl+Q')
  97. exit_button.setStatusTip('Exit application')
  98. exit_button.triggered.connect(self.close)
  99. # About button
  100. about_button = QAction('About', self)
  101. about_button.triggered.connect(self.on_button_clicked)
  102. # Adding buttons to Menu
  103. help_menu.addAction(about_button)
  104. file_menu.addAction(exit_button)
  105. # Path
  106. lbl_url = QLabel()
  107. lbl_url.setText("Insert URL/ID:")
  108. lbl_url.setAlignment(Qt.AlignRight)
  109. lbl_url.setFixedWidth(btnwidth)
  110. lbl_url.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
  111. self.lbl_url_path = QLineEdit()
  112. self.lbl_url_path.setPlaceholderText(
  113. 'https://invidio.us/watch?v=8SdPLG-_wtA')
  114. # Set up callback to update video formats when URL is changed
  115. self.lbl_url_path.textChanged.connect(self.reset_video_formats)
  116. hlayout = QHBoxLayout()
  117. hlayout.addWidget(lbl_url)
  118. hlayout.addWidget(self.lbl_url_path)
  119. # Output path
  120. btn_out_path = QToolButton()
  121. btn_out_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  122. btn_out_path.setText("Select Output Folder")
  123. btn_out_path.setFixedWidth(btnwidth)
  124. btn_out_path.clicked.connect(self.open_output_folder)
  125. self.lbl_out_path = QLineEdit()
  126. self.lbl_out_path.setPlaceholderText("Insert Output Folder Path")
  127. self.lbl_out_path.textChanged.connect(self.update_output_path)
  128. hlayout2 = QHBoxLayout()
  129. hlayout2.addWidget(btn_out_path)
  130. hlayout2.addWidget(self.lbl_out_path)
  131. # Hypervideo path
  132. btn_exec_path = QToolButton()
  133. btn_exec_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  134. btn_exec_path.setText("Select hypervideo")
  135. btn_exec_path.setFixedWidth(btnwidth)
  136. btn_exec_path.clicked.connect(self.select_hyper_dl)
  137. self.lbl_exec_path = QLineEdit(str(self.hypervideo_bin))
  138. self.lbl_exec_path.textChanged.connect(self.update_hypervideo_path)
  139. self.lbl_exec_path.setPlaceholderText("Insert Path to Hypervideo")
  140. hlayout3 = QHBoxLayout()
  141. hlayout3.addWidget(btn_exec_path)
  142. hlayout3.addWidget(self.lbl_exec_path)
  143. # Download button
  144. self.dl_btn = QToolButton()
  145. self.dl_btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  146. self.dl_btn.setText("Download")
  147. self.dl_btn.clicked.connect(self.download_selected)
  148. self.dl_btn.setFixedWidth(btnwidth)
  149. self.dl_btn.setFixedHeight(32)
  150. # Get Formats button
  151. self.fmts_btn = QToolButton()
  152. self.fmts_btn.setText('Get Formats')
  153. self.fmts_btn.setFixedWidth(btnwidth)
  154. self.fmts_btn.setFixedHeight(32)
  155. self.fmts_btn.clicked.connect(self.fill_combo_formats)
  156. # Cancel button
  157. self.cxl_btn = QToolButton()
  158. self.cxl_btn.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  159. self.cxl_btn.setText("Cancel")
  160. self.cxl_btn.clicked.connect(self.cancel_download)
  161. self.cxl_btn.setEnabled(False)
  162. self.cxl_btn.setFixedWidth(btnwidth)
  163. self.cxl_btn.setFixedHeight(32)
  164. # Video FormatCombobox
  165. self.vfms_cmbox = QComboBox()
  166. self.populate_video_format_combobox(self.dflt_fms_menu_items)
  167. self.vfms_cmbox.setFixedHeight(26)
  168. # Progressbar
  169. self.pbar = QProgressBar()
  170. self.pbar.setFixedHeight(16)
  171. self.pbar.setMaximum(100)
  172. self.pbar.setMinimum(0)
  173. self.pbar.setValue(0)
  174. # Layout
  175. btn_layout = QHBoxLayout()
  176. btn_layout.addWidget(self.dl_btn)
  177. btn_layout.addWidget(self.fmts_btn)
  178. btn_layout.addWidget(self.cxl_btn)
  179. vlayout = QVBoxLayout()
  180. vlayout.addLayout(hlayout)
  181. vlayout.addLayout(hlayout2)
  182. vlayout.addLayout(hlayout3)
  183. vlayout.addWidget(self.vfms_cmbox)
  184. vlayout.addWidget(self.pbar)
  185. vlayout.addLayout(btn_layout)
  186. main_widget = QWidget()
  187. main_widget.setLayout(vlayout)
  188. self.setCentralWidget(main_widget)
  189. self.read_settings()
  190. def on_button_clicked(self):
  191. """ Button about """
  192. msg = QMessageBox()
  193. msg.setWindowTitle('About us')
  194. msg.setText(
  195. "<p align='center'>Written with Python3 and PyQt5<br>"
  196. "Version: %s <br> License: %s </p>" %
  197. (__version__, __license__))
  198. msg.setIcon(QMessageBox.Information)
  199. self.show()
  200. msg.exec_()
  201. def closeEvent(self, event):
  202. '''Protected Function for PyQt5
  203. gets called when the user closes the GUI.
  204. '''
  205. self.write_settings()
  206. close = QMessageBox()
  207. close.setIcon(QMessageBox.Question)
  208. close.setWindowTitle('Exit')
  209. close.setText('You sure?')
  210. close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
  211. close = close.exec()
  212. if close == QMessageBox.Yes:
  213. event.accept()
  214. else:
  215. event.ignore()
  216. def read_settings(self):
  217. ''' Read config '''
  218. debugging('Reading settings')
  219. if self.settings.contains('geometry'):
  220. self.setGeometry(self.settings.value('geometry'))
  221. if self.settings.contains('outFolder'):
  222. self.lbl_out_path.setText(self.settings.value('outFolder'))
  223. def write_settings(self):
  224. ''' Save Settings '''
  225. debugging('Writing settings')
  226. self.settings.setValue('outFolder', self.out_folder_path)
  227. self.settings.setValue('geometry', self.geometry())
  228. def update_output_path(self):
  229. ''' Update Path Output '''
  230. self.out_folder_path = self.lbl_out_path.text()
  231. self.show_msg(
  232. "Output path changed to: %s" %
  233. self.lbl_out_path.text())
  234. def update_hypervideo_path(self):
  235. ''' Update Hypervideo Path Output '''
  236. self.hypervideo_bin = self.lbl_exec_path.text()
  237. self.show_msg(
  238. "hypervideo path changed to: %s" %
  239. self.lbl_exec_path.text())
  240. def show_msg(self, message):
  241. ''' Show Message in StatuBar '''
  242. self.statusBar().showMessage(message, msecs=0)
  243. def select_hyper_dl(self):
  244. ''' Select hypervideo executable '''
  245. file_name, _ = QFileDialog.getOpenFileName(
  246. self, "locate hypervideo", "/usr/bin/hypervideo", "exec Files (*)")
  247. debugging('Value of filename is %s' % file_name)
  248. if file_name is not None:
  249. self.lbl_exec_path.setText(file_name)
  250. self.hypervideo_bin = file_name
  251. def open_output_folder(self):
  252. ''' Open out folder path '''
  253. dlg = QFileDialog()
  254. dlg.setFileMode(QFileDialog.Directory)
  255. d_path = dlg.getExistingDirectory()
  256. if d_path:
  257. self.lbl_out_path.setText(d_path)
  258. def populate_video_format_combobox(self, labels):
  259. '''Populate the video format combobox with video formats.
  260. Clear the previous labels.
  261. labels {list} -- list of strings representing
  262. the video format combobox options
  263. '''
  264. self.vfms_cmbox.clear()
  265. for label in labels:
  266. self.vfms_cmbox.addItem(label)
  267. def reset_video_formats(self):
  268. ''' Clean video formast '''
  269. idx = self.vfms_cmbox.currentIndex()
  270. self.populate_video_format_combobox(self.dflt_fms_menu_items)
  271. # Preserve combobox index if possible
  272. if idx > 1:
  273. self.vfms_cmbox.setCurrentIndex(0)
  274. else:
  275. self.vfms_cmbox.setCurrentIndex(idx)
  276. def fill_combo_formats(self):
  277. ''' Scan formats and Add item to combobox '''
  278. self.vfms_cmbox.clear()
  279. if QFile.exists(self.hypervideo_bin):
  280. # Default options
  281. self.vfms_cmbox.addItems(self.dflt_fms_menu_items[0:2])
  282. self.list = []
  283. self.url_catch = self.lbl_url_path.text()
  284. if not self.lbl_url_path.text() == "":
  285. debugging('Scan Formats')
  286. self.proc.start(self.hypervideo_bin, ['-F', self.url_catch])
  287. else:
  288. self.show_msg("URL empty")
  289. else:
  290. self.show_msg("hypervideo missing")
  291. def process_output(self):
  292. ''' Process out '''
  293. try:
  294. output = str(self.proc.readAll(), encoding='utf8').rstrip()
  295. except TypeError:
  296. output = str(self.proc.readAll()).rstrip()
  297. self.list.append(output)
  298. def process_finished(self):
  299. ''' Process Finished '''
  300. out = ','.join(self.list)
  301. out = out.partition("resolution note")[2]
  302. out = out.partition('\n')[2]
  303. mylist = out.rsplit('\n')
  304. debugging('Formats process finished with list: %s' % mylist)
  305. if mylist != ['']:
  306. self.vfms_cmbox.addItems(mylist)
  307. count = self.vfms_cmbox.count()
  308. self.vfms_cmbox.setCurrentIndex(count - 1)
  309. else:
  310. self.show_msg("Formats empty or URL without video")
  311. def download_selected(self):
  312. ''' Download selected video format '''
  313. if QFile.exists(self.hypervideo_bin):
  314. self.pbar.setValue(0)
  315. self.url_catch = self.lbl_url_path.text()
  316. quality = None
  317. options = []
  318. if self.vfms_cmbox.currentText() == self.dflt_fms_menu_items[0]:
  319. quality = 'bestvideo+bestaudio/best'
  320. options.append('-f')
  321. options.append(quality)
  322. elif self.vfms_cmbox.currentText() == self.dflt_fms_menu_items[1]:
  323. quality = '--audio-quality'
  324. options.append('-x')
  325. options.append('--audio-format')
  326. options.append('mp3')
  327. options.append(quality)
  328. options.append('192')
  329. else:
  330. quality = self.vfms_cmbox.currentText().partition(" ")[0]
  331. options.append('-f')
  332. options.append(quality)
  333. if self.url_catch != '':
  334. if quality is not None:
  335. options.append("-o")
  336. options.append("%(title)s.%(ext)s")
  337. options.append(self.url_catch)
  338. self.show_msg("Download started")
  339. debugging('Download Selected Quality: %s' % quality)
  340. debugging('Download URL: %s' % self.url_catch)
  341. self.dl_proc.setWorkingDirectory(self.out_folder_path)
  342. self.dl_proc.start(self.hypervideo_bin, options)
  343. else:
  344. self.show_msg("List of available files is empty")
  345. else:
  346. self.show_msg("URL empty")
  347. else:
  348. self.show_msg("hypervideo missing")
  349. def dl_process_out(self):
  350. ''' Download process out '''
  351. try:
  352. out = str(self.dl_proc.readAll(),
  353. encoding='utf8').rstrip()
  354. except TypeError:
  355. out = str(self.dl_proc.readAll()).rstrip()
  356. out = out.rpartition("[download] ")[2]
  357. self.show_msg("Progress: %s" % out)
  358. self.setWindowTitle(out)
  359. out = out.rpartition("%")[0].rpartition(".")[0]
  360. if not out == "":
  361. try:
  362. pout = int(out)
  363. self.pbar.setValue(pout)
  364. except ValueError:
  365. pass
  366. def msg_dl_finished(self):
  367. ''' Message finished download '''
  368. self.pbar.setValue(100)
  369. msg = QMessageBox()
  370. msg.setWindowTitle('%s' % __title__)
  371. msg.setText('Download done!')
  372. msg.setIcon(QMessageBox.Information)
  373. self.show()
  374. msg.exec_()
  375. def cancel_download(self):
  376. ''' Cancel download'''
  377. if self.dl_proc.state() == QProcess.Running:
  378. debugging('Process is running, will be cancelled')
  379. self.dl_proc.close()
  380. self.show_msg("Download cancelled")
  381. self.pbar.setValue(0)
  382. self.cxl_btn.setEnabled(False)
  383. else:
  384. self.show_msg("Process is not running")
  385. def create_status_bar(self):
  386. ''' Create StatusBar'''
  387. self.statusBar().showMessage("Ready")
  388. def msgbox(self, message):
  389. ''' MessageBox'''
  390. QMessageBox.warning(self, "Message", message)
  391. def debugging(var):
  392. ''' Debugging '''
  393. if DEBUG == 0:
  394. message_debug = print('[debug] %s' % var)
  395. else:
  396. message_debug = None
  397. return message_debug
  398. def ui_style_sheet(self):
  399. ''' Style UI '''
  400. self.mystyle = """
  401. QStatusBar
  402. {
  403. font-family: DejaVu Sans;
  404. font-size: 8pt;
  405. color: #666666;
  406. }
  407. QProgressBar:horizontal
  408. {
  409. border: 1px solid gray;
  410. text-align: top;
  411. padding: 1px;
  412. border-radius: 3px;
  413. background: QLinearGradient(
  414. x1: 0, y1: 0, x2: 1, y2: 0,
  415. stop: 0 #fff,
  416. stop: 0.4999 #eee,
  417. stop: 0.5 #ddd,
  418. stop: 1 #eee );
  419. width: 15px;
  420. }
  421. QProgressBar::chunk:horizontal
  422. {
  423. background: QLinearGradient(
  424. x1: 0, y1: 0, x2: 1, y2: 0,
  425. stop: 0 #5baaf5,
  426. stop: 0.4999 #4ba6f5,
  427. stop: 0.5 #3ba6f5,
  428. stop: 1 #00aaff );
  429. border-radius: 3px;
  430. border: 1px solid black;
  431. }
  432. """
  433. style = self.mystyle
  434. return style