main_ui.py 16 KB

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