main_ui.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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, QClipboard
  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. # Debuging
  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.1'
  40. __license__ = 'GPL-3'
  41. __title__ = 'Simple Hypervideo Download GUI'
  42. class MainWindow(QMainWindow):
  43. def __init__(self):
  44. ''' Initial '''
  45. super(MainWindow, self).__init__()
  46. self.hypervideo_bin = None
  47. self.url_catch = None
  48. self.out_folder_path = '/tmp/'
  49. self.settings = QSettings('YouTubeDL', 'YTDL')
  50. self.setAttribute(Qt.WA_DeleteOnClose)
  51. self.create_status_bar()
  52. pyfile = QStandardPaths.findExecutable("hypervideo")
  53. if not pyfile == "":
  54. debugging('Found executable: %s' % pyfile)
  55. self.hypervideo_bin = pyfile
  56. else:
  57. self.msgbox("hypervideo not found\nPlease install hypervideo")
  58. self.default_formats_menu_items = ['Video/Audio - Best Quality',
  59. 'Audio Only - Best Quality']
  60. self.list = []
  61. self.init_ui()
  62. def init_ui(self):
  63. ''' Initial UI '''
  64. self.setWindowTitle(__title__)
  65. btnwidth = 155
  66. self.cmd = None
  67. self.process = QProcess(self)
  68. self.process.started.connect(lambda: self.show_message("Creating List"))
  69. self.process.started.connect(lambda: self.btn_get_formats.setEnabled(False))
  70. self.process.finished.connect(lambda: self.show_message("Finished creating List"))
  71. self.process.finished.connect(self.process_finished)
  72. self.process.finished.connect(lambda: self.btn_get_formats.setEnabled(True))
  73. self.process.readyRead.connect(self.process_output)
  74. self.download_process = QProcess(self)
  75. self.download_process.setProcessChannelMode(QProcess.MergedChannels)
  76. self.download_process.started.connect(lambda: self.show_message("Download started"))
  77. self.download_process.started.connect(lambda: self.download_button.setEnabled(False))
  78. self.download_process.started.connect(lambda: self.cancel_button.setEnabled(True))
  79. self.download_process.finished.connect(lambda: self.show_message("Download finished"))
  80. self.download_process.finished.connect(lambda: self.download_button.setEnabled(True))
  81. self.download_process.finished.connect(lambda: self.cancel_button.setEnabled(False))
  82. self.download_process.finished.connect(lambda: self.setWindowTitle(__title__))
  83. self.download_process.readyRead.connect(self.dl_process_out)
  84. self.setGeometry(0, 0, 600, 250)
  85. self.setFixedSize(600, 250)
  86. self.setStyleSheet(ui_style_sheet(self))
  87. self.setWindowIcon(QIcon.fromTheme("video-playlist"))
  88. # Menu
  89. main_menu = self.menuBar()
  90. file_menu = main_menu.addMenu('File')
  91. help_menu = main_menu.addMenu('Help')
  92. # Exit button
  93. exit_button = QAction('Exit', self)
  94. exit_button.setShortcut('Ctrl+Q')
  95. exit_button.setStatusTip('Exit application')
  96. exit_button.triggered.connect(self.close)
  97. # About button
  98. about_button = QAction('About', self)
  99. about_button.triggered.connect(self.on_button_clicked)
  100. # Adding buttons to Menu
  101. help_menu.addAction(about_button)
  102. file_menu.addAction(exit_button)
  103. # Path
  104. lbl_url = QLabel()
  105. lbl_url.setText("Insert URL/ID:")
  106. lbl_url.setAlignment(Qt.AlignRight)
  107. lbl_url.setFixedWidth(btnwidth)
  108. lbl_url.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
  109. self.lbl_url_path = QLineEdit()
  110. self.lbl_url_path.setPlaceholderText('https://invidio.us/watch?v=8SdPLG-_wtA')
  111. # Set up callback to update video formats when URL is changed
  112. self.lbl_url_path.textChanged.connect(self.reset_video_formats)
  113. hlayout = QHBoxLayout()
  114. hlayout.addWidget(lbl_url)
  115. hlayout.addWidget(self.lbl_url_path)
  116. # Output path
  117. btn_out_path = QToolButton()
  118. btn_out_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  119. btn_out_path.setText("Select Output Folder")
  120. btn_out_path.setFixedWidth(btnwidth)
  121. btn_out_path.clicked.connect(self.open_output_folder)
  122. self.lbl_out_path = QLineEdit()
  123. self.lbl_out_path.setPlaceholderText("Insert Output Folder Path")
  124. self.lbl_out_path.textChanged.connect(self.update_output_path)
  125. hlayout2 = QHBoxLayout()
  126. hlayout2.addWidget(btn_out_path)
  127. hlayout2.addWidget(self.lbl_out_path)
  128. # Hypervideo path
  129. btn_hypervideo_path = QToolButton()
  130. btn_hypervideo_path.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  131. btn_hypervideo_path.setText("Select hypervideo")
  132. btn_hypervideo_path.setFixedWidth(btnwidth)
  133. btn_hypervideo_path.clicked.connect(self.select_hyper_dl)
  134. self.lbl_hypervideo_path = QLineEdit(str(self.hypervideo_bin))
  135. self.lbl_hypervideo_path.textChanged.connect(self.update_hypervideo_path)
  136. self.lbl_hypervideo_path.setPlaceholderText("Insert Path to Hypervideo")
  137. hlayout3 = QHBoxLayout()
  138. hlayout3.addWidget(btn_hypervideo_path)
  139. hlayout3.addWidget(self.lbl_hypervideo_path)
  140. self.download_button = QToolButton()
  141. self.download_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  142. self.download_button.setText("Download")
  143. self.download_button.clicked.connect(self.download_selected)
  144. self.download_button.setFixedWidth(btnwidth)
  145. self.download_button.setFixedHeight(32)
  146. self.btn_get_formats = QToolButton()
  147. self.btn_get_formats.setText('Get Formats')
  148. self.btn_get_formats.setFixedWidth(btnwidth)
  149. self.btn_get_formats.setFixedHeight(32)
  150. self.btn_get_formats.clicked.connect(self.fill_combo_formats)
  151. self.cancel_button = QToolButton()
  152. self.cancel_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)
  153. self.cancel_button.setText("Cancel")
  154. self.cancel_button.clicked.connect(self.cancel_download)
  155. self.cancel_button.setEnabled(False)
  156. self.cancel_button.setFixedWidth(btnwidth)
  157. self.cancel_button.setFixedHeight(32)
  158. self.video_format_combobox = QComboBox()
  159. self.populate_video_format_combobox(self.default_formats_menu_items)
  160. self.video_format_combobox.setFixedHeight(26)
  161. self.pbar = QProgressBar()
  162. self.pbar.setFixedHeight(16)
  163. self.pbar.setMaximum(100)
  164. self.pbar.setMinimum(0)
  165. self.pbar.setValue(0)
  166. btn_layout = QHBoxLayout()
  167. btn_layout.addWidget(self.download_button)
  168. btn_layout.addWidget(self.btn_get_formats)
  169. btn_layout.addWidget(self.cancel_button)
  170. vlayout = QVBoxLayout()
  171. vlayout.addLayout(hlayout)
  172. vlayout.addLayout(hlayout2)
  173. vlayout.addLayout(hlayout3)
  174. vlayout.addWidget(self.video_format_combobox)
  175. vlayout.addWidget(self.pbar)
  176. vlayout.addLayout(btn_layout)
  177. main_widget = QWidget()
  178. main_widget.setLayout(vlayout)
  179. self.setCentralWidget(main_widget)
  180. # Copy background ID or URL of clipboard
  181. self.clip = QApplication.clipboard()
  182. if self.clip.text().startswith("http"):
  183. self.lbl_url_path.setText(self.clip.text())
  184. self.fill_combo_formats()
  185. self.read_settings()
  186. def on_button_clicked(self):
  187. """ Button about """
  188. msg = QMessageBox()
  189. msg.setWindowTitle('About us')
  190. msg.setText("<p align='center'>Written with Python3 and PyQt5<br>"
  191. "Version: %s <br> License: %s </p>" % (__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_message("Output path changed to: %s" % self.lbl_out_path.text())
  226. def update_hypervideo_path(self):
  227. ''' Update Hypervideo Path Output '''
  228. self.hypervideo_bin = self.lbl_hypervideo_path.text()
  229. self.show_message("hypervideo path changed to: %s" % self.lbl_hypervideo_path.text())
  230. def show_message(self, message):
  231. ''' Show Message in StatuBar '''
  232. self.statusBar().showMessage(message, 0)
  233. def select_hyper_dl(self):
  234. ''' Select hypervideo executable '''
  235. file_name, _ = QFileDialog.getOpenFileName(self, "locate hypervideo",
  236. "/usr/bin/hypervideo", "exec Files (*)")
  237. if file_name:
  238. self.lbl_hypervideo_path.setText(file_name)
  239. self.hypervideo_bin = file_name
  240. def open_output_folder(self):
  241. ''' Open out folder path '''
  242. dlg = QFileDialog()
  243. dlg.setFileMode(QFileDialog.Directory)
  244. d_path = dlg.getExistingDirectory()
  245. if d_path:
  246. self.lbl_out_path.setText(d_path)
  247. def populate_video_format_combobox(self, labels):
  248. '''Populate the video format combobox with video formats. Clear the previous labels.
  249. labels {list} -- list of strings representing the video format combobox options
  250. '''
  251. self.video_format_combobox.clear()
  252. for label in labels:
  253. self.video_format_combobox.addItem(label)
  254. def reset_video_formats(self):
  255. ''' Clean video formast '''
  256. idx = self.video_format_combobox.currentIndex()
  257. self.populate_video_format_combobox(self.default_formats_menu_items)
  258. # preserve combobox index if possible
  259. if idx > 1:
  260. self.video_format_combobox.setCurrentIndex(0)
  261. else:
  262. self.video_format_combobox.setCurrentIndex(idx)
  263. def fill_combo_formats(self):
  264. ''' Scan formats and Add item to combobox '''
  265. self.video_format_combobox.clear()
  266. if QFile.exists(self.hypervideo_bin):
  267. # Default options
  268. self.video_format_combobox.addItems(self.default_formats_menu_items[0:2])
  269. self.list = []
  270. self.url_catch = self.lbl_url_path.text()
  271. if not self.lbl_url_path.text() == "":
  272. debugging('Scan Formats')
  273. self.process.start(self.hypervideo_bin, ['-F', self.url_catch])
  274. else:
  275. self.show_message("URL empty")
  276. else:
  277. self.show_message("hypervideo missing")
  278. def process_output(self):
  279. ''' Process out '''
  280. try:
  281. output = str(self.process.readAll(), encoding='utf8').rstrip()
  282. except TypeError:
  283. output = str(self.process.readAll()).rstrip()
  284. self.list.append(output)
  285. def process_finished(self):
  286. ''' Process Finished '''
  287. out = ','.join(self.list)
  288. out = out.partition("resolution note")[2]
  289. out = out.partition('\n')[2]
  290. mylist = out.rsplit('\n')
  291. debugging('Formats process finished with list: %s' % mylist)
  292. if mylist != ['']:
  293. self.video_format_combobox.addItems(mylist)
  294. count = self.video_format_combobox.count()
  295. self.video_format_combobox.setCurrentIndex(count-1)
  296. else:
  297. self.show_message("Formats empty or URL without video")
  298. def download_selected(self):
  299. ''' Download selected video format '''
  300. if QFile.exists(self.hypervideo_bin):
  301. self.pbar.setValue(0)
  302. self.url_catch = self.lbl_url_path.text()
  303. quality = None
  304. if self.video_format_combobox.currentText() == self.default_formats_menu_items[0]:
  305. quality = 'bestvideo+bestaudio/best'
  306. options = []
  307. options.append('-f')
  308. options.append(quality)
  309. elif self.video_format_combobox.currentText() == self.default_formats_menu_items[1]:
  310. quality = '--audio-quality'
  311. options = []
  312. options.append('-x')
  313. options.append('--audio-format')
  314. options.append('mp3')
  315. options.append(quality)
  316. options.append('192')
  317. else:
  318. quality = self.video_format_combobox.currentText().partition(" ")[0]
  319. options = []
  320. options.append('-f')
  321. options.append(quality)
  322. if self.url_catch != '':
  323. if quality is not None:
  324. options.append("-o")
  325. options.append("%(title)s.%(ext)s")
  326. options.append(self.url_catch)
  327. self.show_message("Download started")
  328. debugging('Download Selected Quality: %s' % quality)
  329. debugging('Download URL: %s' % self.url_catch)
  330. self.download_process.setWorkingDirectory(self.out_folder_path)
  331. self.download_process.start(self.hypervideo_bin, options)
  332. else:
  333. self.show_message("List of available files is empty")
  334. else:
  335. self.show_message("URL empty")
  336. else:
  337. self.show_message("hypervideo missing")
  338. def dl_process_out(self):
  339. ''' Download process out '''
  340. try:
  341. out = str(self.download_process.readAll(), encoding='utf8').rstrip()
  342. except TypeError:
  343. out = str(self.download_process.readAll()).rstrip()
  344. out = out.rpartition("[download] ")[2]
  345. self.show_message("Progress: %s" % out)
  346. self.setWindowTitle(out)
  347. out = out.rpartition("%")[0].rpartition(".")[0]
  348. if not out == "":
  349. try:
  350. pout = int(out)
  351. self.pbar.setValue(pout)
  352. except ValueError:
  353. pass
  354. def cancel_download(self):
  355. ''' Cancel download'''
  356. if self.download_process.state() == QProcess.Running:
  357. debugging('Process is running, will be cancelled')
  358. self.download_process.close()
  359. self.show_message("Download cancelled")
  360. self.pbar.setValue(0)
  361. self.cancel_button.setEnabled(False)
  362. else:
  363. self.show_message("Process is not running")
  364. def create_status_bar(self):
  365. ''' Create StatusBar'''
  366. self.statusBar().showMessage("Ready")
  367. def msgbox(self, message):
  368. ''' MessageBox'''
  369. QMessageBox.warning(self, "Message", message)
  370. def debugging(var):
  371. ''' Debugging '''
  372. if DEBUG == 0:
  373. message_debug = print('[debug] %s' % var)
  374. else:
  375. message_debug = None
  376. return message_debug
  377. def ui_style_sheet(self):
  378. return """
  379. QStatusBar
  380. {
  381. font-family: Helvetica;
  382. font-size: 8pt;
  383. color: #666666;
  384. }
  385. QProgressBar:horizontal {
  386. border: 1px solid gray;
  387. text-align: top;
  388. padding: 1px;
  389. border-radius: 3px;
  390. background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,
  391. stop: 0 #fff,
  392. stop: 0.4999 #eee,
  393. stop: 0.5 #ddd,
  394. stop: 1 #eee );
  395. width: 15px;
  396. }
  397. QProgressBar::chunk:horizontal
  398. {
  399. background: QLinearGradient( x1: 0, y1: 0, x2: 1, y2: 0,
  400. stop: 0 #5baaf5,
  401. stop: 0.4999 #4ba6f5,
  402. stop: 0.5 #3ba6f5,
  403. stop: 1 #00aaff );
  404. border-radius: 3px;
  405. border: 1px solid black;
  406. }
  407. """