#!/usr/bin/python2 try: import os, getopt, sys, tempfile, shutil, subprocess from subprocess import CalledProcessError except: print('Eunektes requires the following standard modules to be present in order to function: os, getopt, sys, tempfile, shutil, subprocess\nIf your version of python2 doesn\'t include these, try upgrading to a more recent version of python2') sys.exit(2) try: from PyKDE4.kdecore import * from PyKDE4.kdeui import * from PyKDE4.kio import KDirSelectDialog from PyKDE4 import kdecore, kdeui from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4 import QtCore, QtGui except: print("Eunektes requires PyQt4 and PyKDE4 in order to work, please make sure these are installed and try to run Eunektes again.") sys.exit(2) class StreamCopyError(Exception): pass class MountFailure(Exception): pass class BadUserInput(Exception): pass class DumperError(Exception): pass class ShowTitles(QDialog): def __init__(self, parent): QDialog.__init__(self, parent) self.title_tree = QTreeWidget() self.title_tree.setColumnCount(2) self.layout = QVBoxLayout() self.setLayout(self.layout) self.layout.addWidget(self.title_tree) self.show() def populate(self): for title in self._data['track']: title_widget = QTreeWidgetItem() title_widget.setText(0, 'Title: %s' % title['ix']) title_widget.setText(1, '%s' % self.convert_seconds_string(title['length'])) self.title_tree.addTopLevelItem(title_widget) for chapter in title['chapter']: chapter_widget = QTreeWidgetItem() chapter_widget.setText(0, 'Chapter: %s' % chapter['ix']) chapter_widget.setText(1, '%s' % self.convert_seconds_string(chapter['length'])) title_widget.addChild(chapter_widget) def convert_seconds_string(self, seconds): t_seconds = int(seconds) % 60 minutes = (int(seconds) - t_seconds)/ 60 t_minutes = int(minutes) % 60 hours = int(t_minutes) / 60 return_string = "" if hours > 0: return_string = str(hours) + 'h ' if minutes > 0: return_string = return_string + str(t_minutes) + 'm ' elif hours > 0 and minutes == 0: return_string = return_string + str(t_minutes) + 'm ' return_string = return_string + str(t_seconds)+ 's' return return_string def setData(self, data): self._data = data class RunCommand(QThread): def reset(self): self.with_output = False self.command = '' self.results = {} def check_output(self): try: output = subprocess.check_output(self.command,shell=True) output = output.decode('utf-8') output = output.strip() self.results['ecode']=0 self.results['output']=output except CalledProcessError as e: self.results['ecode'] = e.returncode self.results['output'] = e.output def check_call(self): try: subprocess.check_call(self.command, shell=True) self.results['ecode'] = 0 except CalledProcessError as e: self.results['ecode'] = e.returncode def run(self): if self.with_output == True: self.check_output() else: self.check_call() class Eunektes(QWidget): def __init__(self): QWidget.__init__(self, parent=None) self.title_name = '' self.window = KMainWindow() self.window.setCentralWidget(self) self.stacked_layout = QStackedLayout(self) self.page1 = QWidget() self.page2 = QWidget() self.input_layout = QFormLayout(self.page1) self.progress_layout = QVBoxLayout(self.page2) self.stacked_layout.addWidget(self.page1) #index 0 self.stacked_layout.addWidget(self.page2) #index 1 self.stacked_layout.setCurrentIndex(0) self.device_widget = QWidget(self) self.device_layout = QHBoxLayout() self.device_widget.setLayout(self.device_layout) self.device_title = QPushButton() self.device_title.clicked.connect(self.show_title_tree) self.device_combo = QComboBox(self) devfiles = os.listdir('/dev/') drives = [] for f in devfiles: if f[0:2] == 'sr': drives.append('/dev/' +f) drives.sort() self.device_combo.addItems(drives) self.device_combo.editTextChanged.connect(self.start_timer) self.device_combo.currentIndexChanged.connect(self.start_timer) self.device_combo.setEditable(True) self.device_combo.setToolTip("""This is where you put the path to your optical device \nAn ISO or folder with VIDEO_TS may also work.""") self.device_layout.addWidget(self.device_combo) self.device_layout.addWidget(self.device_title) self.input_layout.addRow("Device: ",self.device_widget) self.titles_line_edit = QLineEdit(self) self.titles_line_edit.setToolTip("""You enter the title(s):chapters you want here.\nexample: 2:1-5,3:2,2:6-10\nTitles are separated by commas, if you want to specify chapters\nthen you put a colon, then the starting chapter 'dash' ending chapter\nIf the ending chapter is not specified, it plays from start chapter to the end of the title.\nThis way, you can get different parts of the same title if you need to\nexample: 2:1-9,2:10-17,2:18-25\nThis would get title 2, chapters 1-9 to a file, title 2, chapters 10-17 to another file, and so on.""") self.input_layout.addRow("Title(s): ", self.titles_line_edit) self.copy_first_checkbox = QCheckBox(self) self.copy_first_checkbox.setChecked(False) self.copy_first_checkbox.setToolTip("""This will mirror the disk to your hard drive with vobcopy before proceeding. \nIt can be slower or faster depending on various things.\nCertain dvd copy protection make vobcopy fail, so it's not as reliable as letting mencoder read directly from the disk.""") self.input_layout.addRow("Copy to disk first?: ",self.copy_first_checkbox) self.destination_folder_line_edit = MyDirRequester(self) self.destination_folder_line_edit.setText(os.environ['HOME']) self.input_layout.addRow("Destination Directory: ",self.destination_folder_line_edit) self.file_name_line_edit = QLineEdit(self) self.file_name_line_edit.setToolTip("""The output filename, without extension ... if multiple titles\n and/or chapters are involved, it will add that info accordingly.\nIt can also accept a comma separated list of titles - the number of names must match the number of titles if you go this route.""") self.input_layout.addRow("Output Filename:", self.file_name_line_edit) fps_tooltip = "You'll have to play with this one to get it right ... there's no reliable way to detect it so you'll have to experiment and see what works best for the title. At worst if you get it wrong I think the subtitles might be out of sync. I could be wrong though." self.fps24_button = QRadioButton(self) self.fps24_button.setText("24fps film (non-telecine)") self.fps24_button.setToolTip(fps_tooltip) self.fps24_button.setChecked(True) self.fps30_button = QRadioButton(self) self.fps30_button.setText("30fps progressive (telecined)") self.fps30_button.setToolTip(fps_tooltip) self.framerate_group = QButtonGroup(self) self.framerate_group.addButton(self.fps24_button) self.framerate_group.addButton(self.fps30_button) self.input_layout.addRow(self.fps24_button, self.fps30_button) self.input_button_box = QDialogButtonBox(self) self.input_button_box.addButton(QDialogButtonBox.Reset) self.input_button_box.button(QDialogButtonBox.Reset).clicked.connect(self.reset_form) self.run_button = QPushButton(self) self.run_button.setText("&Run") self.input_button_box.addButton(self.run_button, QDialogButtonBox.YesRole) self.run_button.clicked.connect(self.run) self.input_layout.addRow(self.input_button_box) self.progress_label = QLabel() self.progress_label.setAlignment(Qt.AlignHCenter) self.progress_table = QTableWidget() self.progress_table.setColumnCount(3) #self.progress_table.setShowGrid(False) self.progress_table.horizontalHeader().setVisible(False) self.progress_table.verticalHeader().setVisible(False) self.progress_button_box = QDialogButtonBox(self) self.progress_button_box.addButton(QDialogButtonBox.Cancel) self.progress_button_box.button(QDialogButtonBox.Cancel).clicked.connect(self.cancel_run) self.progress_button_box.addButton(QDialogButtonBox.Ok) self.progress_button_box.button(QDialogButtonBox.Ok).setVisible(False) self.progress_button_box.button(QDialogButtonBox.Ok).clicked.connect(self.progress_okay) self.progress_button_box.button(QDialogButtonBox.Cancel).setEnabled(False) self.progress_layout.addWidget(self.progress_label) self.progress_layout.addWidget(self.progress_table) self.progress_layout.addWidget(self.progress_button_box) self.preview_title_timer = QTimer() self.preview_title_timer.timeout.connect(self.update_device_title) self.window.show() self.update_device_title() self.check_dependencies() def quit_q_loop(self): print("Quitting Event Loop") self.q.quit() def check_dependencies(self): try: self.run_command_nolog('which mplayer &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find mplayer on your system - exiting.") try: self.run_command_nolog('which mencoder &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find mencoder on your system - exiting") try: self.run_command_nolog('which lsdvd &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find lsdvd on your system - disabling title info feature", False) self.device_title.setEnabled(False) try: self.run_command_nolog('which dvdxchap &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find dvdxchap on your system - exiting") try: self.run_command_nolog('which mkvmerge &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find mkvmerge on your system - exiting") try: self.run_command_nolog('which vobcopy &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find vobcopy on your system - disabling copy first feature", False) self.copy_first_checkbox.setEnabled(False) try: self.run_command_nolog('which ffmpeg &> /dev/null') except CalledProcessError: self.show_error("Eunektes can't find ffmpeg on your system - disabling re-encode problematic video feature", False) self.has_ffmpeg = False else: self.has_ffmpeg = True def start_timer(self): self.preview_title_timer.start(500) def show_title_tree(self): #if self.device_title == 'FOLDER_MODE': # tree = self.run-command_with_output('lsdvd -Oy -c %s') tree = self.run_command_with_output('lsdvd -Oy -c %s' % self.device_combo.currentText()) tree = eval(tree[8:]) dialog = ShowTitles(self) dialog.setData(tree) dialog.populate() dialog.show() def cancel_run(self): """I should probably implement this eventually.""" pass def add_row(self, title, chapters, message): current = self.progress_table.rowCount() self.progress_table.insertRow(current) if chapters != False: title_string = 'Title: %s - Chapters: %s' % (str(title), str(chapters)) else: title_string = 'Title: %s' % str(title) title_item = QTableWidgetItem(title_string) self.progress_table.setItem(current, 0, title_item) self.progress_table.setItem(current, 1, QTableWidgetItem(message)) self.progress_table.setCellWidget(current, 2, QProgressBar()) self.progress_table.cellWidget(current, 2).setRange(0, 0) self.progress_table.resizeColumnsToContents() self.progress_table.scrollToItem(title_item) return current def update_row(self, row, success=True): if success: self.progress_table.cellWidget(row, 2).setMaximum(1) self.progress_table.cellWidget(row, 2).setValue(1) else: #self.progress_table.removeCellWidget(row, 2) #self.progress_table.cellWidget(row, 2).destroy() self.progress_table.cellWidget(row, 2).setVisible(False) self.progress_table.setCellWidget(row, 2, QLabel('Failed')) self.progress_table.cellWidget(row, 2).setVisible(True) #self.progress_table.cellWidget(row, 2).hide() #self.progress_table.setCellWidget()row, 2, QTableWidgetItem('Failed')) def run_command_with_output(self,command): self.vlog("Now running command: %s" % command) return self.run_command_with_output_nolog(command) def run_command_with_output_nolog(self, command): q = QEventLoop() run_command_thread = RunCommand() run_command_thread.reset() run_command_thread.finished.connect(q.quit) run_command_thread.command = command run_command_thread.with_output = True run_command_thread.start() q.exec_() results = run_command_thread.results output = subprocess.check_output(command,shell=True) output = output.decode('utf-8') output = output.strip() return output def update_device_title(self, *myint): try: title_name = self.get_title_name(self.device_combo.currentText()) if title_name.strip() == '': if os.path.isdir(str(self.device_combo.currentText()) + '/VIDEO_TS'): title_name = 'FOLDER_MODE' if title_name.strip() != '': self.device_title.setText(title_name) else: self.device_title.setText('') return title_name except CalledProcessError: self.device_title.setText('') def reset_form(self): self.fps24_button.setChecked(True) self.titles_line_edit.setText('') self.copy_first_checkbox.setChecked(False) self.file_name_line_edit.setText('') def run_command(self, command): self.vlog("Now running command: %s" % command) self.run_command_nolog(command) def run_command_nolog(self, command): q = QEventLoop() run_command_thread = RunCommand() run_command_thread.reset() run_command_thread.finished.connect(q.quit) run_command_thread.command = command run_command_thread.with_output = True run_command_thread.start() q.exec_() results = run_command_thread.results if results['ecode'] != 0: raise CalledProcessError(results['ecode'], '') def reset_progress(self): for x in range(self.progress_table.rowCount()): self.progress_table.removeRow(x) #self.progress_table.setColumnCount(3) self.progress_label.setText('') self.stacked_layout.setCurrentIndex(1) self.progress_button_box.button(QDialogButtonBox.Ok).setVisible(False) self.progress_button_box.button(QDialogButtonBox.Cancel).setVisible(True) def run(self): try: device = str(self.device_combo.currentText()) #this way, it ignores blkid failing when it is a directory with video_ts folder_mode = False if os.path.isdir(device + '/' + 'VIDEO_TS'): folder_mode = True if not folder_mode: if device.strip() == '': raise BadUserInput('Invalid device specified') title_name = self.get_title_name(device) if title_name.strip() == '': raise BadUserInput("Couldn't get title_name from device") else: title_name='FOLDER_MODE' if str(self.titles_line_edit.text()).strip() == '': raise BadUserInput("Title selection seems invalid - remember that no spaces are allowed") #May add further title error checking later ... but it will prove to be rather complicated self.reset_progress() unsplit_tracks = str(self.titles_line_edit.text()).split(',') tracks = list() for x, track in enumerate(unsplit_tracks): tempdict = dict() try: tempdict['number'], tempdict['chapters'] = track.split(':') except ValueError: tempdict['number'] = track tempdict['chapters'] = False finally: tracks.append(tempdict) del tempdict file_names = str(self.file_name_line_edit.text()).split(',') for x, file_name in enumerate(file_names): if str(file_name).strip() == '': if x == 0: file_names[x] = title_name else: file_names[x] = title_name + '_' + titles[x]['number'] if titles[x]['chapters'] != False: file_names[x] = file_names[x] + '_' + titles[x]['chapters'] if len(tracks) != len(file_names) and len(file_names) != 1: raise BadInputError("You need to specify as many file names as you do tracks, else specify one file name and it will alter accordingly.") """TODO: make this call cancel_run when that function is functional, instead of exiting""" if len(tracks) > 1: if len(file_names) == len(tracks): for x, track in enumerate(tracks): tracks[x]['file_name'] = file_names[x] elif len(file_names) == 1: for x, track in enumerate(tracks): tracks[x]['file_name'] = file_names[0] + '_' + tracks[x]['number'] if tracks[x]['chapters'] != False: tracks[x]['file_name'] = tracks[x]['file_name'] + '_' + tracks[x]['chapters'] elif len(tracks) == 1: tracks[0]['file_name'] = file_names[0] if not folder_mode: if self.copy_first_checkbox.isChecked() == True: copy_first = True else: copy_first = False else: copy_first = False if self.fps24_button.isChecked() == True: fps = '24000/1001' else: fps = '30000/1001' destination_folder = str(self.destination_folder_line_edit.url()) if not folder_mode: self.progress_label.setText(title_name) else: self.progress_label.setText('Folder mode - title not available') if copy_first: copy_first_row = self.add_row('*',False,'Mirror disk to hard drive') mirror_dir = self.do_vobcopy(destination_folder, device, title_name) device = '%s/%s' % (mirror_dir, title_name.upper()) self.update_row(copy_first_row) for track in tracks: self.dump(track['number'],track['chapters'], track['file_name'], title_name, destination_folder, fps, device) self.progress_button_box.button(QDialogButtonBox.Ok).setVisible(True) self.progress_button_box.button(QDialogButtonBox.Cancel).setVisible(False) if copy_first: shutil.rmtree(mirror_dir) except BadUserInput as e: self.show_error("Bad user input detected: \n%s" % e.message) except DumperError as e: self.show_error("Dumper had an error", False) self.update_row(self.progress_table.rowCount()-1, False) self.progress_button_box.button(QDialogButtonBox.Ok).setVisible(True) self.progress_button_box.button(QDialogButtonBox.Cancel).setVisible(False) def get_title_name(self, device): try: title_name = self.run_command_with_output_nolog("""blkid -o export %s | grep LABEL | cut -d '=' -f 2""" % device) return title_name except CalledProcessError: self.show_error("blkid failed to obtain the disk title for the device %s" % device) def show_error(self,error_message, exit=True): print(error_message) self.log(error_message) QMessageBox.critical(self,"A Critical Error Has Occurred",error_message) #if exit: #sys.exit(2) def do_vobcopy(self, destination_folder, device, title_name): if not os.path.exists('/media/%s' % title_name): self.show_error("Eunektes now expects disks to be automounted to the appropriate directory, /media/