def okButtonClicked(self): print("OK button clicked") p = QProcess() env = QProcessEnvironment.systemEnvironment() env.insert("SUDO_ASKPASS", os.path.dirname(__file__) + "/askpass.py") # FIXME: This is not working p.setProcessEnvironment(env) p.setProgram("sudo") p.setArguments(["-A", "-E", os.path.dirname(__file__) + "/adduser.sh", self.username.text(), self.password.text()]) p.start() p.waitForFinished() err = p.readAllStandardError().data().decode() err = err.replace("QKqueueFileSystemWatcherEngine::addPaths: open: No such file or directory", "").strip() # FIXME: Where is this coming from, remove it at the root of the problem if err and err != "": print(err) msg = QMessageBox() msg.setIcon(QMessageBox.Critical) msg.setText(err) # msg.setInformativeText('More information') msg.setWindowTitle("Error") msg.exec_() out = p.readAllStandardOutput().data().decode() if out: print(out) if "Successfully added" in out: msg = QMessageBox() msg.setIcon(QMessageBox.Information) msg.setText("Successfully added the user.") # msg.setInformativeText('More information') msg.setWindowTitle(" ") msg.exec_() self.close() print("p.exitStatus():", p.exitStatus()) if p.exitStatus() != 0: print("An error occured; TODO: Handle it in the GUI")
def test_version(request): """Test invocation with --version argument.""" args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination proc = QProcess() proc.setProcessChannelMode(QProcess.SeparateChannels) proc.start(sys.executable, args) ok = proc.waitForStarted(2000) assert ok ok = proc.waitForFinished(10000) stdout = bytes(proc.readAllStandardOutput()).decode('utf-8') print(stdout) stderr = bytes(proc.readAllStandardError()).decode('utf-8') print(stderr) assert ok if qtutils.version_check('5.9'): # Segfaults on exit with Qt 5.7 assert proc.exitStatus() == QProcess.NormalExit match = re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout, re.MULTILINE) assert match is not None
def create_process( program: str, args: tp.Optional[tp.List[str]] = None, workdir: tp.Optional[tp.Union[str, Path]] = None) -> tp.Iterator[QProcess]: """ Creates a new process. The method does not return immediately. Instead it waits until the process finishes. If the process gets interrupted by the user (e.g. by calling the ProcessManager's shutdown() method), the ProcessTerminatedError exception gets raised. Example usage: with ProcessManager.create_process(prog, args) as proc: # modify/configure the QProcess object # process is started after the when block is exited """ args = [] if args is None else args proc = QProcess() yield proc if workdir is not None: with local.cwd(workdir): ProcessManager.start_process(proc, program, args) else: ProcessManager.start_process(proc, program, args) proc.waitForFinished(-1) if proc.exitStatus() != QProcess.NormalExit: raise ProcessTerminatedError()
class _Converter: """_Converter class to provide conversion functionality.""" def __init__(self, library_path): """Class initializer.""" self._library_path = library_path self._process = QProcess() def setup_converter(self, reader, finisher, process_channel): """Set up the QProcess object.""" self._process.setProcessChannelMode(process_channel) self._process.readyRead.connect(reader) self._process.finished.connect(finisher) def start_converter(self, cmd): """Start the encoding process.""" self._process.start(self._library_path, cmd) def stop_converter(self): """Terminate the encoding process.""" self._process.terminate() if self.converter_is_running: self._process.kill() def converter_finished_disconnect(self, connected): """Disconnect the QProcess.finished method.""" self._process.finished.disconnect(connected) def close_converter(self): """Call QProcess.close method.""" self._process.close() def kill_converter(self): """Call QProcess.kill method.""" self._process.kill() def converter_state(self): """Call QProcess.state method.""" return self._process.state() def converter_exit_status(self): """Call QProcess.exit_status method.""" return self._process.exitStatus() def read_converter_output(self): """Call QProcess.readAll method.""" return str(self._process.readAll()) @property def converter_is_running(self): """Return QProcess state.""" return self._process.state() == QProcess.Running
def has_bash(): """ Test if bash is available. """ process = QProcess() process.start("which bash") process.waitForStarted() process.waitForFinished() if process.exitStatus() == QProcess.NormalExit: return bool(process.readAll()) return False
def run(self, argv, error_message, in_build_dir=False, timeout=30000): """ Execute a command and capture the output. """ if in_build_dir: project = self._project saved_cwd = os.getcwd() build_dir = project.path_from_user(project.build_dir) build_dir = QDir.toNativeSeparators(build_dir) os.chdir(build_dir) self._message_handler.verbose_message( "{0} is now the current directory".format(build_dir)) else: saved_cwd = None self._message_handler.verbose_message("Running '{0}'".format( ' '.join(argv))) QCoreApplication.processEvents() process = QProcess() process.readyReadStandardOutput.connect( lambda: self._message_handler.progress_message( QTextCodec.codecForLocale().toUnicode( process.readAllStandardOutput()).strip())) stderr_output = QByteArray() process.readyReadStandardError.connect( lambda: stderr_output.append(process.readAllStandardError())) process.start(argv[0], argv[1:]) finished = process.waitForFinished(timeout) if saved_cwd is not None: os.chdir(saved_cwd) self._message_handler.verbose_message( "{0} is now the current directory".format(saved_cwd)) if not finished: raise UserException(error_message, process.errorString()) if process.exitStatus() != QProcess.NormalExit or process.exitCode( ) != 0: raise UserException( error_message, QTextCodec.codecForLocale().toUnicode(stderr_output).strip())
def run(self, argv, error_message, in_build_dir=False): """ Execute a command and capture the output. """ if in_build_dir: project = self._project saved_cwd = os.getcwd() build_dir = project.path_from_user(project.build_dir) build_dir = QDir.toNativeSeparators(build_dir) os.chdir(build_dir) self._message_handler.verbose_message( "{0} is now the current directory".format(build_dir)) else: saved_cwd = None self._message_handler.verbose_message( "Running '{0}'".format(' '.join(argv))) QCoreApplication.processEvents() process = QProcess() process.readyReadStandardOutput.connect( lambda: self._message_handler.progress_message( QTextCodec.codecForLocale().toUnicode( process.readAllStandardOutput()).strip())) stderr_output = QByteArray() process.readyReadStandardError.connect( lambda: stderr_output.append(process.readAllStandardError())) process.start(argv[0], argv[1:]) finished = process.waitForFinished() if saved_cwd is not None: os.chdir(saved_cwd) self._message_handler.verbose_message( "{0} is now the current directory".format(saved_cwd)) if not finished: raise UserException(error_message, process.errorString()) if process.exitStatus() != QProcess.NormalExit or process.exitCode() != 0: raise UserException(error_message, QTextCodec.codecForLocale().toUnicode(stderr_output).strip())
def test_version(request): """Test invocation with --version argument.""" args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination proc = QProcess() proc.setProcessChannelMode(QProcess.SeparateChannels) proc.start(sys.executable, args) ok = proc.waitForStarted(2000) assert ok ok = proc.waitForFinished(2000) assert ok assert proc.exitStatus() == QProcess.NormalExit output = bytes(proc.readAllStandardOutput()).decode('utf-8') print(output) assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', output) is not None
def test_version(request): """Test invocation with --version argument.""" args = ['-m', 'qutebrowser', '--version'] + _base_args(request.config) # can't use quteproc_new here because it's confused by # early process termination proc = QProcess() proc.setProcessChannelMode(QProcess.SeparateChannels) proc.start(sys.executable, args) ok = proc.waitForStarted(2000) assert ok ok = proc.waitForFinished(2000) assert ok assert proc.exitStatus() == QProcess.NormalExit stdout = bytes(proc.readAllStandardOutput()).decode('utf-8') print(stdout) stderr = bytes(proc.readAllStandardError()).decode('utf-8') print(stderr) assert re.search(r'^qutebrowser\s+v\d+(\.\d+)', stdout) is not None
def run_qprocess(cmd: str, *args: str, cwd=None) -> str: """Run a shell command synchronously using QProcess. Args: cmd: The command to run. args: Any arguments passed to the command. cwd: Directory of the command to run in. Returns: The standard output of the command. Raises: OSError on failure. """ process = QProcess() if cwd is not None: process.setWorkingDirectory(cwd) process.start(cmd, args) if not process.waitForFinished(): raise OSError("Error waiting for process") if process.exitStatus() != QProcess.NormalExit or process.exitCode() != 0: stderr = qbytearray_to_str(process.readAllStandardError()).strip() raise OSError(stderr) return qbytearray_to_str(process.readAllStandardOutput()).strip()
def load_cr3(path) -> QPixmap: """Extract the thumbnail from the image and initialize QPixmap""" process = QProcess() process.start(f"exiftool -b -JpgFromRaw {path}") process.waitForFinished() if process.exitStatus() != QProcess.NormalExit or process.exitCode() != 0: stderr = process.readAllStandardError() raise ValueError(f"Error calling exiftool: '{stderr.data().decode()}'") handler = QImageReader(process, "jpeg".encode()) handler.setAutoTransform(True) process.closeWriteChannel() process.terminate() # Extract QImage from QImageReader and convert to QPixmap pixmap = QPixmap() pixmap.convertFromImage(handler.read()) return pixmap
def load_frame(path) -> QPixmap: """Extract the first frame from the video and initialize QPixmap""" process = QProcess() process.start( f"ffmpeg -loglevel quiet -i {path} -vframes 1 -f image2 pipe:1") process.waitForFinished() if process.exitStatus() != QProcess.NormalExit or process.exitCode() != 0: stderr = process.readAllStandardError() raise ValueError(f"Error calling ffmpeg: '{stderr.data().decode()}'") handler = QImageReader(process, "jpeg".encode()) handler.setAutoTransform(True) process.closeWriteChannel() process.terminate() # Extract QImage from QImageReader and convert to QPixmap pixmap = QPixmap() pixmap.convertFromImage(handler.read()) return pixmap
class AsyncProcess: """ Wraps a QProcess and provides a neat await-able interface. This is not "cross-task-safe", i.e. one AsyncProcess should not be manipulated concurrently by more than one task. """ def __init__(self, args=None, qprocess=None, scheduler=None): self._scheduler = scheduler if qprocess: self._process = qprocess assert not args else: self._process = QProcess() self._process.setProgram(args[0]) self._process.setArguments(args[1:]) self._current_error_occured = self._error_outside_await self._process.errorOccurred.connect(self._error_outside_await) self._stored_error = None def _error_outside_await(self, error): self._stored_error = error def _set_error_occured(self, swapin): swapout = self._current_error_occured self._process.errorOccurred.disconnect(swapout) self._process.errorOccurred.connect(swapin) self._current_error_occured = swapin def _error_handling_future(self, source_signal, result_callback, exit_callback=None) -> Future: """ Create future responding to QProcess errors. *result_callback* is connected to *source_signal*. *exit_callback* is connected to QProcess.finished. Any error occurring until completion of the future will set an exception on the future and thus consume the error. Signal connections are undone when the future is done. """ def error_occurred(error): fut.set_exception(AsyncProcessError(error)) def process_finished(*args): exit_callback() def disconnect(fut): source_signal.disconnect(result_callback) if exit_callback: self._process.finished.disconnect(process_finished) self._set_error_occured(self._error_outside_await) fut = asynker.Future(self._scheduler) source_signal.connect(result_callback) if exit_callback: self._process.finished.connect(process_finished) self._set_error_occured(error_occurred) fut.add_done_callback(disconnect) # Check if there already happened an error if self._stored_error is not None: error_occurred(self._stored_error) self._stored_error = None return fut def start(self, mode=QProcess.ReadWrite) -> Future: """ Start the process. *mode* is passed to QProcess.start(). """ def started(): fut.set_result(self._process.processId()) assert self._process.state() == QProcess.NotRunning fut = self._error_handling_future(self._process.started, started) self._process.start(mode) return fut def _read(self, ready_read_signal, read_method) -> Future: def data_available(): fut.set_result(read_method().data()) fut = self._error_handling_future(ready_read_signal, data_available, data_available) # Clear out buffered data immediately data = read_method().data() if data: fut.set_result(data) elif not data and self._process.state() == QProcess.NotRunning: fut.set_result(b'') return fut def read_stdout(self) -> Future: """ Read some binary data from the process's stdout channel. """ return self._read(self._process.readyReadStandardOutput, self._process.readAllStandardOutput) def read_stderr(self) -> Future: """ Read some binary data from the process's stdout channel. """ return self._read(self._process.readyReadStandardError, self._process.readAllStandardError) def write(self, data) -> Future: """ Write *data* to the standard input of the attached process. """ def bytes_written(n=None): nonlocal pending_bytes if n is None and pending_bytes: fut.set_exception(AsyncProcessLostWrite) elif n: pending_bytes -= n if not pending_bytes: fut.set_result(None) assert self._process.state() == QProcess.Running fut = self._error_handling_future(self._process.bytesWritten, bytes_written, bytes_written) pending_bytes = len(data) amount = self._process.write(data) if amount == -1: fut.set_exception(OSError) pending_bytes -= amount if not pending_bytes: fut.set_result(None) return fut def write_eof(self): """ Close (send EOF to) the standard input of the attached process. """ self._process.closeWriteChannel() def finish(self) -> Future: """ Wait until the process exits. Returns an (exit_code, QProcess.ExitStatus) tuple. """ def finished(exit_code, exit_status): fut.set_result((exit_code, exit_status)) fut = self._error_handling_future(self._process.finished, finished) if self._process.state() == QProcess.NotRunning: finished(self._process.exitCode(), self._process.exitStatus()) return fut def running(self): return self._process.state() == QProcess.Running
class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. _started: Whether the underlying process is started. _proc: The underlying QProcess. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__(self, what, *, verbose=False, additional_env=None, parent=None): super().__init__(parent) self._what = what self.verbose = verbose self._started = False self.cmd = None self.args = None self._proc = QProcess(self) self._proc.error.connect(self.on_error) self._proc.error.connect(self.error) self._proc.finished.connect(self.on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self.on_started) self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) @pyqtSlot(QProcess.ProcessError) def on_error(self, error): """Show a message if there was an error while spawning.""" msg = ERROR_STRINGS[error] message.error("Error while spawning {}: {}".format(self._what, msg)) @pyqtSlot(int, QProcess.ExitStatus) def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: message.error("{} crashed!".format(self._what.capitalize())) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info("{} exited successfully.".format( self._what.capitalize())) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. message.error("{} exited with status {}.".format( self._what.capitalize(), code)) stderr = bytes(self._proc.readAllStandardError()).decode('utf-8') stdout = bytes(self._proc.readAllStandardOutput()).decode('utf-8') if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) @pyqtSlot() def on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self._started self._started = True def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) log.procs.debug("Executing: {}".format(fake_cmdline)) if self.verbose: message.info('Executing: ' + fake_cmdline) def start(self, cmd, args, mode=None): """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) if mode is None: self._proc.start(cmd, args) else: self._proc.start(cmd, args, mode) self._proc.closeWriteChannel() def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, cwd) if ok: log.procs.debug("Process started.") self._started = True else: message.error("Error while spawning {}: {}".format( self._what, ERROR_STRINGS[self._proc.error()])) def exit_status(self): return self._proc.exitStatus()
class Snapshoter(QObject): def __init__(self, session): QObject.__init__(self) self.session = session self.git_exec = 'git' self.gitdir = '.ray-snapshots' self.exclude_path = 'info/exclude' self.history_path = "session_history.xml" self.max_file_size = 50 #in Mb self.next_snapshot_name = '' self._rw_snapshot = '' self.changes_checker = QProcess() self.changes_checker.readyReadStandardOutput.connect( self.changesCheckerStandardOutput) self.adder_process = QProcess() self.adder_process.finished.connect(self.save_step_1) self.adder_process.readyReadStandardOutput.connect( self.adderStandardOutput) self._adder_aborted = False self.git_process = QProcess() self.git_process.readyReadStandardOutput.connect(self.standardOutput) self.git_process.readyReadStandardError.connect(self.standardError) self.git_command = '' self._n_file_changed = 0 self._n_file_treated = 0 self._changes_counted = False self.next_function = None self.error_function = None def changesCheckerStandardOutput(self): standard_output = self.changes_checker.readAllStandardOutput().data() self._n_file_changed += len(standard_output.decode().split('\n')) - 1 def adderStandardOutput(self): standard_output = self.adder_process.readAllStandardOutput().data() Terminal.snapshoterMessage(standard_output, ' add -A -v') if not self._n_file_changed: return self._n_file_treated += len(standard_output.decode().split('\n')) - 1 self.session.sendGui('/ray/gui/server/progress', self._n_file_treated / self._n_file_changed) def standardError(self): standard_error = self.git_process.readAllStandardError().data() Terminal.snapshoterMessage(standard_error, self.git_command) def standardOutput(self): standard_output = self.git_process.readAllStandardOutput().data() Terminal.snapshoterMessage(standard_output, self.git_command) def getGitDir(self): if not self.session.path: raise NameError("attempting to save with no session path !!!") return "%s/%s" % (self.session.path, self.gitdir) def runGitProcess(self, *all_args): return self.runGitProcessAt(self.session.path, *all_args) def runGitProcessAt(self, spath, *all_args): self.git_command = '' for arg in all_args: self.git_command += ' %s' % arg err = ray.Err.OK git_args = self.getGitCommandListAt(spath, *all_args) self.git_process.start(self.git_exec, git_args) if not self.git_process.waitForFinished(2000): self.git_process.kill() err = ray.Err.SUBPROCESS_UNTERMINATED else: if self.git_process.exitStatus(): err = ray.Err.SUBPROCESS_CRASH elif self.git_process.exitCode(): err = ray.Err.SUBPROCESS_EXITCODE if err and self.error_function: self.error_function(err, ' '.join(all_args)) return not bool(err) def getGitCommandList(self, *args): return self.getGitCommandListAt(self.session.path, *args) def getGitCommandListAt(self, spath, *args): first_args = [ '--work-tree', spath, '--git-dir', "%s/%s" % (spath, self.gitdir) ] return first_args + list(args) def getHistoryFullPath(self): return "%s/%s/%s" % (self.session.path, self.gitdir, self.history_path) def getHistoryXmlDocumentElement(self): if not self.isInit(): return None file_path = self.getHistoryFullPath() xml = QDomDocument() try: history_file = open(file_path, 'r') xml.setContent(history_file.read()) history_file.close() except BaseException: return None SNS_xml = xml.documentElement() if SNS_xml.tagName() != 'SNAPSHOTS': return None return SNS_xml def list(self, client_id=""): SNS_xml = self.getHistoryXmlDocumentElement() if not SNS_xml: return [] nodes = SNS_xml.childNodes() all_tags = [] all_snaps = [] prv_session_name = self.session.name for i in range(nodes.count()): node = nodes.at(i) el = node.toElement() if client_id: client_nodes = node.childNodes() for j in range(client_nodes.count()): client_node = client_nodes.at(j) client_el = client_node.toElement() if client_el.attribute('client_id') == client_id: break else: continue ref = el.attribute('ref') name = el.attribute('name') rw_sn = el.attribute('rewind_snapshot') rw_name = "" session_name = el.attribute('session_name') # don't list snapshot from client before session renamed if client_id and session_name != self.session.name: client = self.session.getClient(client_id) if (client and (client.prefix_mode == ray.PrefixMode.SESSION_NAME)): continue ss_name = "" if session_name != prv_session_name: ss_name = session_name prv_session_name = session_name if not ref.replace('_', '').isdigit(): continue if '\n' in name: name = "" if not rw_sn.replace('_', '').isdigit(): rw_sn = "" if rw_sn: for snap in all_snaps: if snap[0] == rw_sn and not '\n' in snap[1]: rw_name = snap[1] break all_snaps.append((ref, name)) snapsss = fullRefForGui(ref, name, rw_sn, rw_name, ss_name) all_tags.append(snapsss) all_tags.reverse() #return all_tags.__reversed__() return all_tags def getTagDate(self): date_time = QDateTime.currentDateTimeUtc() date = date_time.date() time = date_time.time() tagdate = "%s_%s_%s_%s_%s_%s" % (date.year(), date.month(), date.day(), time.hour(), time.minute(), time.second()) return tagdate def writeHistoryFile(self, date_str, snapshot_name='', rewind_snapshot=''): if not self.session.path: return ray.Err.NO_SESSION_OPEN file_path = self.getHistoryFullPath() xml = QDomDocument() try: history_file = open(file_path, 'r') xml.setContent(history_file.read()) history_file.close() except: pass if xml.firstChild().isNull(): SNS_xml = xml.createElement('SNAPSHOTS') xml.appendChild(SNS_xml) else: SNS_xml = xml.firstChild() snapshot_el = xml.createElement('Snapshot') snapshot_el.setAttribute('ref', date_str) snapshot_el.setAttribute('name', snapshot_name) snapshot_el.setAttribute('rewind_snapshot', rewind_snapshot) snapshot_el.setAttribute('session_name', self.session.name) snapshot_el.setAttribute('VERSION', ray.VERSION) for client in self.session.clients + self.session.trashed_clients: client_el = xml.createElement('client') client.writeXmlProperties(client_el) client_el.setAttribute('client_id', client.client_id) for client_file_path in client.getProjectFiles(): base_path = client_file_path.replace("%s/" % self.session.path, '', 1) file_xml = xml.createElement('file') file_xml.setAttribute('path', base_path) client_el.appendChild(file_xml) snapshot_el.appendChild(client_el) SNS_xml.appendChild(snapshot_el) try: history_file = open(file_path, 'w') history_file.write(xml.toString()) history_file.close() except: return ray.Err.CREATE_FAILED return ray.Err.OK def getExcludeFileFullPath(self): return "%s/%s/%s" % (self.session.path, self.gitdir, self.exclude_path) def writeExcludeFile(self): file_path = self.getExcludeFileFullPath() try: exclude_file = open(file_path, 'w') except: return ray.Err.CREATE_FAILED contents = "" contents += "# This file is generated by ray-daemon at each snapshot\n" contents += "# Don't edit this file.\n" contents += "# If you want to add/remove files managed by git\n" contents += "# Create/Edit .gitignore in the session folder\n" contents += "\n" contents += "%s\n" % self.gitdir contents += "\n" contents += "# Globally ignored extensions\n" session_ignored_extensions = ray.getGitIgnoredExtensions() session_ign_list = session_ignored_extensions.split(' ') session_ign_list = tuple(filter(bool, session_ign_list)) # write global ignored extensions for extension in session_ign_list: contents += "*%s\n" % extension for client in self.session.clients: cext_list = client.ignored_extensions.split(' ') if not extension in cext_list: contents += "!%s.%s/**/*%s\n" % (gitStringer( client.getPrefixString()), gitStringer( client.client_id), extension) contents += "!%s.%s.**/*%s\n" % (gitStringer( client.getPrefixString()), gitStringer( client.client_id), extension) contents += '\n' contents += "# Extensions ignored by clients\n" # write client specific ignored extension for client in self.session.clients: cext_list = client.ignored_extensions.split(' ') for extension in cext_list: if not extension: continue if extension in session_ignored_extensions: continue contents += "%s.%s/**/*%s\n" % (gitStringer( client.getPrefixString()), gitStringer( client.client_id), extension) contents += "%s.%s.**/*%s\n" % (gitStringer( client.getPrefixString()), gitStringer( client.client_id), extension) contents += '\n' contents += "# Too big Files\n" no_check_list = (self.gitdir) # check too big files for foldername, subfolders, filenames in os.walk(self.session.path): subfolders[:] = [d for d in subfolders if d not in no_check_list] if foldername == "%s/%s" % (self.session.path, self.gitdir): continue for filename in filenames: if filename.endswith(session_ign_list): if os.path.islink(filename): short_folder = foldername.replace( self.session.path + '/', '', 1) line = gitStringer("%s/%s" % (short_folder, filename)) contents += '!%s\n' % line # file with extension globally ignored but # unignored by its client will not be ignored # and that is well as this. continue if os.path.islink(filename): continue try: file_size = os.path.getsize( os.path.join(foldername, filename)) except: continue if file_size > self.max_file_size * 1024**2: if foldername == self.session.path: line = gitStringer(filename) else: short_folder = foldername.replace( self.session.path + '/', '', 1) line = gitStringer("%s/%s" % (short_folder, filename)) contents += "%s\n" % line try: exclude_file.write(contents) exclude_file.close() except: return ray.Err.CREATE_FAILED return ray.Err.OK def isInit(self): if not self.session.path: return False return os.path.isfile( "%s/%s/%s" % (self.session.path, self.gitdir, self.exclude_path)) def hasChanges(self): if not self.session.path: return False if not self.isInit(): return True if self.changes_checker.state(): self.changes_checker.kill() self._n_file_changed = 0 self._n_file_treated = 0 self._changes_counted = True args = self.getGitCommandList('ls-files', '--exclude-standard', '--others', '--modified') self.changes_checker.start(self.git_exec, args) self.changes_checker.waitForFinished(2000) return bool(self._n_file_changed) def canSave(self): if not self.session.path: return False if not self.isInit(): if not self.runGitProcess('init'): return False user_name = os.getenv('USER') if not user_name: user_name = 'someone' machine_name = socket.gethostname() if not machine_name: machine_name = 'somewhere' if not self.runGitProcess('config', 'user.email', '%s@%s' % (user_name, machine_name)): return False user_name = os.getenv('USER') if not user_name: user_name = 'someone' if not self.runGitProcess('config', 'user.name', user_name): return False if not self.isInit(): return False return True def errorQuit(self, err): if self.error_function: self.error_function(err) self.error_function = None def save(self, name='', rewind_snapshot='', next_function=None, error_function=None): self.next_snapshot_name = name self._rw_snapshot = rewind_snapshot self.next_function = next_function self.error_function = error_function if not self.canSave(): Terminal.message("can't snapshot") return err = self.writeExcludeFile() if err: self.errorQuit(err) return self._adder_aborted = False if not self._changes_counted: self.hasChanges() self._changes_counted = False if self._n_file_changed: all_args = self.getGitCommandList('add', '-A', '-v') self.adder_process.start(self.git_exec, all_args) else: self.save_step_1() # self.adder_process.finished is connected to self.save_step_1 def save_step_1(self): if self._adder_aborted: if self.next_function: self.next_function(aborted=True) return if self._n_file_changed: if not self.runGitProcess('commit', '-m', 'ray'): return if (self._n_file_changed or self.next_snapshot_name or self._rw_snapshot): ref = self.getTagDate() if not self.runGitProcess('tag', '-a', ref, '-m', 'ray'): return err = self.writeHistoryFile(ref, self.next_snapshot_name, self._rw_snapshot) if err: if self.error_function: self.error_function(err) # not really a reply, not strong. self.session.sendGui( '/reply', '/ray/session/list_snapshots', fullRefForGui(ref, self.next_snapshot_name, self._rw_snapshot)) self.error_function = None self.next_snapshot_name = '' self._rw_snapshot = '' if self.next_function: self.next_function() def load(self, spath, snapshot, error_function): self.error_function = error_function snapshot_ref = snapshot.partition('\n')[0].partition(':')[0] if not self.runGitProcessAt(spath, 'reset', '--hard'): return False if not self.runGitProcessAt(spath, 'checkout', snapshot_ref): return False return True def loadClientExclusive(self, client_id, snapshot, error_function): self.error_function = error_function SNS_xml = self.getHistoryXmlDocumentElement() if not SNS_xml: self.error_function(ray.Err.NO_SUCH_FILE, self.getHistoryFullPath()) return False nodes = SNS_xml.childNodes() client_path_list = [] for i in range(nodes.count()): node = nodes.at(i) el = node.toElement() if el.attribute('ref') != snapshot: continue client_nodes = node.childNodes() for j in range(client_nodes.count()): client_node = client_nodes.at(j) client_el = client_node.toElement() if client_el.attribute('client_id') != client_id: continue file_nodes = client_node.childNodes() for k in range(file_nodes.count()): file_node = file_nodes.at(k) file_el = file_node.toElement() file_path = file_el.attribute('path') if file_path: client_path_list.append(file_path) if not self.runGitProcess('reset', '--hard'): return False if not self.runGitProcess('checkout', snapshot, '--', * client_path_list): return False return True def abort(self): if not self.adder_process.state(): return self.setAutoSnapshot(False) self._adder_aborted = True self.adder_process.terminate() def setAutoSnapshot(self, bool_snapshot): auto_snap_file = "%s/%s/prevent_auto_snapshot" % (self.session.path, self.gitdir) file_exists = bool(os.path.exists(auto_snap_file)) if bool_snapshot: if file_exists: try: os.remove(auto_snap_file) except PermissionError: return else: if not file_exists: contents = "# This file prevent auto snapshots for this session (RaySession)\n" contents += "# remove it if you want auto snapshots back" try: file = open(auto_snap_file, 'w') file.write(contents) file.close() except PermissionError: return def isAutoSnapshotPrevented(self): auto_snap_file = "%s/%s/prevent_auto_snapshot" % (self.session.path, self.gitdir) return bool(os.path.exists(auto_snap_file))
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() # lists self.file_list = [] for entry in os.scandir(os.path.dirname(sys.argv[0])): if entry.is_file(): if entry.name.endswith( ".txt") and not entry.name == "mychannels.txt": self.file_list.append(entry.name) self.file_list.sort(key=str.lower) flist = '\n'.join(self.file_list) print(f'found lists:\n{flist}') check = self.check_libmpv("libmpv") if not check: print("libmpv not found\n") self.msgbox("libmpv not found\nuse 'sudo apt-get install libmpv1'") sys.exit() else: print("found libmpv") self.check_mpv("mpv") self.setAttribute(Qt.WA_NoSystemBackground, True) self.setStyleSheet("QMainWindow {background-color: 'black';}") self.osd_font_size = 28 self.colorDialog = None self.settings = QSettings("TVPlayer2", "settings") self.own_list = [] self.own_key = 0 self.default_key = 0 self.default_list = [] self.urlList = [] self.channel_list = [] self.channels_files_list = [] self.link = "" self.menulist = [] self.recording_enabled = False self.is_recording = False self.recname = "" self.timeout = "60" self.tout = 60 self.outfile = "/tmp/TV.mp4" self.myARD = "" self.channelname = "" self.mychannels = [] self.channels_menu = QMenu() self.processR = QProcess() self.processR.started.connect(self.getPIDR) self.processR.finished.connect(self.timer_finished) self.processR.isRunning = False self.pid = None self.processW = QProcess() self.processW.started.connect(self.getPIDW) self.processW.finished.connect(self.recfinished) self.processW.isRunning = False self.container = QWidget(self) self.setCentralWidget(self.container) self.container.setAttribute(Qt.WA_DontCreateNativeAncestors) self.container.setAttribute(Qt.WA_NativeWindow) self.container.setContextMenuPolicy(Qt.CustomContextMenu) self.container.customContextMenuRequested[QPoint].connect( self.contextMenuRequested) self.setAcceptDrops(True) self.mediaPlayer = mpv.MPV(log_handler=self.logger, input_cursor=False, osd_font_size=self.osd_font_size, cursor_autohide=2000, cursor_autohide_fs_only=True, osd_color='#d3d7cf', osd_blur=2, osd_bold=True, wid=str(int(self.container.winId())), config=False, profile="libmpv", hwdec=False, vo="x11") self.mediaPlayer.set_loglevel('fatal') self.own_file = "mychannels.txt" if os.path.isfile(self.own_file): self.mychannels = open(self.own_file).read() ### remove empty lines self.mychannels = os.linesep.join( [s for s in self.mychannels.splitlines() if s]) with open(self.own_file, 'w') as f: f.write(self.mychannels) self.fullscreen = False self.setMinimumSize(320, 180) self.setGeometry(100, 100, 480, round(480 / ratio)) self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setWindowTitle("TV Player & Recorder") self.setWindowIcon(QIcon.fromTheme("multimedia-video-player")) self.myinfo = """<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><!--StartFragment--><span style=" font-size:xx-large; font-weight:600;">TVPlayer2</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">©2020<br /><a href="https://github.com/Axel-Erfurt"><span style=" color:#0000ff;">Axel Schneider</span></a></p> <h3 style=" margin-top:14px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:large; font-weight:600;">Keyboard shortcuts:</span></h3> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">q = Exit<br />f = toggle Fullscreen</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">u = Play url from the clipboard</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Mouse wheel = change window size</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">↑ = volume up</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">↓ = volume down</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">m = Ton an/aus</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">h = Mouse pointer on / off</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">r = Recording with timer</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">w = Recording without timer<br />s = Stop recording</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">--------------------------------------</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">1 bis 0 = own channels (1 to 10)</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">→ = Channels +</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">+ = own channel +</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">← = Channel -</p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">- = own channel -</p>""" print("Welcome to the TV Player & Recorder") if self.is_tool("ffmpeg"): print("found ffmpeg\nrecording available") self.recording_enabled = True else: self.msgbox("ffmpeg not foundn\n no recording available") self.show() self.readSettings() self.createMenu() def check_libmpv(self, mlib): cmd = f'ldconfig -p | grep {mlib}' try: result = check_output(cmd, stderr=STDOUT, shell=True).decode("utf-8") except CalledProcessError: return False if not mlib in result: return False else: return True def check_mpv(self, mlib): cmd = f'pip3 list | grep {mlib}' try: result = check_output(cmd, stderr=STDOUT, shell=True).decode("utf-8") if not mlib in result: return False else: return True except CalledProcessError as exc: result = exc.output return False def logger(self, loglevel, component, message): print('[{}] {}: {}'.format(loglevel, component, message), file=sys.stderr) def editOwnChannels(self): mfile = f"{os.path.join(os.path.dirname(sys.argv[0]))}/mychannels.txt" #QDesktopServices.openUrl(QUrl(f"file://{mfile}")) self.list_editor = editor_intern.Viewer() self.list_editor.show() def addToOwnChannels(self): k = "Name" dlg = QInputDialog() myname, ok = dlg.getText(self, 'Dialog', 'Name:', QLineEdit.Normal, k, Qt.Dialog) if ok: if os.path.isfile(self.own_file): with open(self.own_file, 'a') as f: f.write(f"\n{myname},{self.link}") self.channelname = myname else: self.msgbox(f"{self.own_file} does not exist!") def readSettings(self): print("reading configuation ...") if self.settings.contains("geometry"): self.setGeometry( self.settings.value("geometry", QRect(26, 26, 200, 200))) else: self.setGeometry(100, 100, 480, 480 / ratio) if self.settings.contains("lastUrl") and self.settings.contains( "lastName"): self.link = self.settings.value("lastUrl") self.channelname = self.settings.value("lastName") self.mediaPlayer.show_text(self.channelname, duration="4000", level=None) self.mediaPlayer.play(self.link) print(f"current station: {self.channelname}\nURL: {self.link}") else: if len(self.own_list) > 0: self.play_own(0) if self.settings.contains("volume"): vol = self.settings.value("volume") print("set volume to", vol) self.mediaPlayer.volume = (int(vol)) def writeSettings(self): print("writing configuation file ...") self.settings.setValue("geometry", self.geometry()) self.settings.setValue("lastUrl", self.link) self.settings.setValue("lastName", self.channelname) self.settings.setValue("volume", self.mediaPlayer.volume) self.settings.sync() def mouseDoubleClickEvent(self, event): self.handleFullscreen() event.accept() def getBufferStatus(self): print(self.mediaPlayer.bufferStatus()) def createMenu(self): myMenu = self.channels_menu.addMenu("Channels") myMenu.setIcon(QIcon.fromTheme(mytv)) if len(self.mychannels) > 0: for ch in self.mychannels.splitlines(): name = ch.partition(",")[0] url = ch.partition(",")[2] self.own_list.append(f"{name},{url}") a = QAction(name, self, triggered=self.playTV) a.setIcon(QIcon.fromTheme(mybrowser)) a.setData(url) myMenu.addAction(a) ### other lists for x in range(len(self.file_list)): newMenu = self.channels_menu.addMenu( os.path.splitext(os.path.basename(self.file_list[x]))[0]) newMenu.setIcon(QIcon.fromTheme(mytv)) channelList = open(self.file_list[x], 'r').read().splitlines() for ch in channelList: name = ch.partition(",")[0] url = ch.partition(",")[2] self.channel_list.append(f"{name},{url}") self.own_list.append(f"{name},{url}") a = QAction(name, self, triggered=self.playTV) a.setIcon(QIcon.fromTheme(mybrowser)) a.setData(url) newMenu.addAction(a) ############################# if self.recording_enabled: self.channels_menu.addSection("Recording") self.tv_record = QAction(QIcon.fromTheme("media-record"), "record with Timer (r)", triggered=self.record_with_timer) self.channels_menu.addAction(self.tv_record) self.tv_record2 = QAction(QIcon.fromTheme("media-record"), "record without Timer (w)", triggered=self.record_without_timer) self.channels_menu.addAction(self.tv_record2) self.tv_record_stop = QAction( QIcon.fromTheme("media-playback-stop"), "stop recording (s)", triggered=self.stop_recording) self.channels_menu.addAction(self.tv_record_stop) self.channels_menu.addSeparator() self.about_action = QAction(QIcon.fromTheme("help-about"), "Info (i)", triggered=self.handleAbout, shortcut="i") self.channels_menu.addAction(self.about_action) self.channels_menu.addSeparator() self.url_action = QAction(QIcon.fromTheme("browser"), "play URL from clipboard (u)", triggered=self.playURL) self.channels_menu.addAction(self.url_action) self.channels_menu.addSection("Settings") self.color_action = QAction(QIcon.fromTheme("preferences-color"), "Color Settings (c)", triggered=self.showColorDialog) self.channels_menu.addAction(self.color_action) self.channels_menu.addSeparator() self.channels_menu.addSeparator() self.channels_menu.addSection("add / edit Channels") self.addChannelAction = QAction(QIcon.fromTheme("add"), "add current channel", triggered=self.addToOwnChannels) self.channels_menu.addAction(self.addChannelAction) self.editChannelAction = QAction(QIcon.fromTheme("text-editor"), "edit own channels", triggered=self.editOwnChannels) self.channels_menu.addAction(self.editChannelAction) self.channels_menu.addSeparator() self.quit_action = QAction(QIcon.fromTheme("application-exit"), "Exit (q)", triggered=self.handleQuit) self.channels_menu.addAction(self.quit_action) def showTime(self): t = str(datetime.now())[11:16] self.mediaPlayer.show_text(t, duration="4000", level=None) def dragEnterEvent(self, event): event.acceptProposedAction() def dropEvent(self, event): if event.mimeData().hasUrls(): url = event.mimeData().urls()[0].toString() print(f"new link dropped: '{url}'") self.link = url.strip() self.mediaPlayer.stop() self.mediaPlayer.play(self.link) elif event.mimeData().hasText(): mydrop = event.mimeData().text().strip() if ("http") in mydrop: print(f"new link dropped: '{mydrop}'") self.link = mydrop self.mediaPlayer.play(self.link) event.acceptProposedAction() def recfinished(self): print("recording will be stopped") def is_tool(self, name): tool = QStandardPaths.findExecutable(name) if tool != "": return True else: return False def getPIDR(self): print("pid", self.processR.processId()) self.pid = self.processR.processId() def getPIDW(self): print("pid", self.processW.processId()) self.pid = self.processW.processId() def record_without_timer(self): if not self.recording_enabled == False: if QFile(self.outfile).exists: print("delete file " + self.outfile) QFile(self.outfile).remove else: print("the file " + self.outfile + " does not exist") self.recname = self.channelname print("recording in file /tmp/TV.mp4") self.mediaPlayer.show_text("record without timer", duration="3000", level=None) self.is_recording = True self.recordChannelW() def record_with_timer(self): if not self.recording_enabled == False: if QFile(self.outfile).exists: print("lösche Datei " + self.outfile) QFile(self.outfile).remove else: print("the file " + self.outfile + " does not exist") infotext = '<i>temporary recording in file: /tmp/TV.mp4</i> \ <br><b><font color="#a40000";>The storage location and file name are determined\nafter the recording is finished.</font></b> \ <br><br><b>Example:</b><br>60s (60 seconds)<br>120m (120 minutes)' dlg = QInputDialog() tout, ok = dlg.getText(self, 'Duration:', infotext, \ QLineEdit.Normal, "90m", Qt.Dialog) if ok: self.tout = str(tout) self.is_recording = True self.recordChannel() else: print("recording cancelled") def recordChannel(self): self.processR.isRunning = True self.recname = self.channelname cmd = f'timeout {str(self.tout)} ffmpeg -y -i {self.link.replace("?sd=10&rebase=on", "")} -bsf:a aac_adtstoasc -vcodec copy -c copy -crf 50 "{self.outfile}"' print("recording in /tmp with timeout: " + str(self.tout)) self.mediaPlayer.show_text(f"Recording with timer {str(self.tout)}", duration="3000", level=None) self.is_recording = True self.processR.start(cmd) def recordChannelW(self): self.processW.isRunning = True self.recname = self.channelname cmd = f'ffmpeg -y -i {self.link.replace("?sd=10&rebase=on", "")} -bsf:a aac_adtstoasc -vcodec copy -c copy -crf 50 "{self.outfile}"' self.mediaPlayer.show_text("Recording", duration="3000", level=None) self.is_recording = True self.processW.start(cmd) ################################################################ def saveMovie(self): self.fileSave() def fileSave(self): infile = QFile(self.outfile) path, _ = QFileDialog.getSaveFileName( self, "Save as...", QDir.homePath() + "/Videos/" + self.recname + ".mp4", "Video (*.mp4)") if os.path.exists(path): os.remove(path) if (path != ""): savefile = path if QFile(savefile).exists: QFile(savefile).remove() print("saving " + savefile) if not infile.copy(savefile): QMessageBox.warning( self, "Error", "cannot write file %s:\n%s." % (path, infile.errorString())) if infile.exists: infile.remove() def stop_recording(self): print("StateR:", self.processR.state()) print("StateW:", self.processW.state()) if self.is_recording == True: if self.processW.isRunning: print("recording will be stopped") cmd = f"kill -9 {self.pid}" print(cmd, "(stop ffmpeg)") QProcess().execute(cmd) if self.processW.exitStatus() == 0: self.processW.isRunning = False self.saveMovie() else: print("no recording") def timer_finished(self): print("Timer ended\nrecording will be stopped") self.processR.isRunning = False self.is_recording = False self.saveMovie() def playURL(self): clip = QApplication.clipboard() self.link = clip.text().strip() self.mediaPlayer.play(self.link) def handleError(self, loglevel, message): print('{}: {}'.format(loglevel, message), file=sys.stderr) def handleMute(self): if not self.mediaPlayer.mute: self.mediaPlayer.mute = True print("muted") else: self.mediaPlayer.mute = False print("not muted") def handleAbout(self): QMessageBox.about(self, "TVPlayer2", self.myinfo) def handleFullscreen(self): if self.fullscreen == True: self.fullscreen = False print("no fullscreen") else: self.rect = self.geometry() self.showFullScreen() QApplication.setOverrideCursor(Qt.ArrowCursor) self.fullscreen = True print("fullscreen") if self.fullscreen == False: self.showNormal() self.setGeometry(self.rect) QApplication.setOverrideCursor(Qt.BlankCursor) self.handleCursor() def handleCursor(self): if QApplication.overrideCursor() == Qt.ArrowCursor: QApplication.setOverrideCursor(Qt.BlankCursor) else: QApplication.setOverrideCursor(Qt.ArrowCursor) def handleQuit(self): self.mediaPlayer.quit self.writeSettings() print("Goodbye ...") app.quit() sys.exit() def keyPressEvent(self, e): if e.key() == Qt.Key_Q: self.handleQuit() elif e.key() == Qt.Key_H: self.handleCursor() elif e.key() == Qt.Key_F: self.handleFullscreen() elif e.key() == Qt.Key_M: self.handleMute() elif e.key() == Qt.Key_I: self.handleAbout() elif e.key() == Qt.Key_U: self.playURL() elif e.key() == Qt.Key_R: self.record_with_timer() elif e.key() == Qt.Key_S: self.stop_recording() elif e.key() == Qt.Key_T: self.showTime() elif e.key() == Qt.Key_E: self.getEPG_detail() elif e.key() == Qt.Key_W: self.record_without_timer() elif e.key() == Qt.Key_C: self.showColorDialog() elif e.key() == Qt.Key_1: self.play_own(0) elif e.key() == Qt.Key_2: self.play_own(1) elif e.key() == Qt.Key_3: self.play_own(2) elif e.key() == Qt.Key_4: self.play_own(3) elif e.key() == Qt.Key_5: self.play_own(4) elif e.key() == Qt.Key_6: self.play_own(5) elif e.key() == Qt.Key_7: self.play_own(6) elif e.key() == Qt.Key_8: self.play_own(7) elif e.key() == Qt.Key_9: self.play_own(8) elif e.key() == Qt.Key_0: self.play_own(9) elif e.key() == Qt.Key_A: self.playARD() elif e.key() == Qt.Key_Z: self.playZDF() elif e.key() == Qt.Key_Right: self.play_next(self.default_key + 1) elif e.key() == Qt.Key_Plus: self.play_own(self.own_key + 1) elif e.key() == Qt.Key_Left: self.play_next(self.default_key - 1) elif e.key() == Qt.Key_Minus: if not self.own_key == 0: self.play_own(self.own_key - 1) elif e.key() == Qt.Key_Up: if self.mediaPlayer.volume < 160: self.mediaPlayer.volume = (self.mediaPlayer.volume + 5) print("Volume:", self.mediaPlayer.volume) self.mediaPlayer.show_text( f"Volume: {self.mediaPlayer.volume}") elif e.key() == Qt.Key_Down: if self.mediaPlayer.volume > 5: self.mediaPlayer.volume = (self.mediaPlayer.volume - 5) print("Volume:", self.mediaPlayer.volume) self.mediaPlayer.show_text( f"Volume: {self.mediaPlayer.volume}") else: e.accept() def contextMenuRequested(self, point): self.channels_menu.exec_(self.mapToGlobal(point)) def playFromKey(self, url): self.link = url self.mediaPlayer.play(self.link) def playTV(self): action = self.sender() self.link = action.data().replace("\n", "") self.channelname = action.text() self.mediaPlayer.show_text(self.channelname, duration="4000", level=None) if self.channelname in self.channel_list: self.default_key = self.channel_list.index(self.channelname) else: self.own_key = self.own_list.index( f"{self.channelname},{self.link}") print(f"current channel: {self.channelname}\nURL: {self.link}") self.mediaPlayer.play(self.link) def play_own(self, channel): if not channel > len(self.own_list) - 1: self.own_key = channel self.link = self.own_list[channel].split(",")[1] self.channelname = self.own_list[channel].split(",")[0] self.mediaPlayer.show_text(self.channelname, duration="4000", level=None) print("own channel:", self.channelname, "\nURL:", self.link) self.mediaPlayer.play(self.link) else: print(f"channel {channel} not exists") def play_next(self, channel): if not channel > len(self.default_list) - 1: self.default_key = channel self.link = self.default_list[channel].split(",")[1] self.channelname = self.default_list[channel].split(",")[0] self.mediaPlayer.show_text(self.channelname, duration="4000", level=None) print(f"current channel: {self.channelname}\nURL: {self.link}") self.mediaPlayer.play(self.link) else: self.play_next(0) def play_previous(self, channel): if not channel == 0: self.default_key = channel self.link = self.default_list[channel].split(",")[1] self.channelname = self.default_list[channel].split(",")[0] self.mediaPlayer.show_text(self.channelname, duration="4000", level=None) print(f"current channel: {self.channelname}\nURL: {self.link}") self.mediaPlayer.play(self.link) else: self.play_next(len(self.default_list)) def closeEvent(self, event): event.accept() def msgbox(self, message): QMessageBox.warning(self, "Message", message) def wheelEvent(self, event): mwidth = self.frameGeometry().width() mscale = round(event.angleDelta().y() / 6) self.resize(mwidth + mscale, round((mwidth + mscale) / ratio)) event.accept() def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton: self.move(event.globalPos() \ - QPoint(round(self.frameGeometry().width() / 2), \ round(self.frameGeometry().height() / 2))) event.accept() def setBrightness(self): self.mediaPlayer.brightness = self.brightnessSlider.value() def setContrast(self): self.mediaPlayer.contrast = self.contrastSlider.value() def setHue(self): self.mediaPlayer.hue = self.hueSlider.value() def setSaturation(self): self.mediaPlayer.saturation = self.saturationSlider.value() def showColorDialog(self): if self.colorDialog is None: self.brightnessSlider = QSlider(Qt.Horizontal) self.brightnessSlider.setRange(-100, 100) self.brightnessSlider.setValue(self.mediaPlayer.brightness) self.brightnessSlider.valueChanged.connect(self.setBrightness) self.contrastSlider = QSlider(Qt.Horizontal) self.contrastSlider.setRange(-100, 100) self.contrastSlider.setValue(self.mediaPlayer.contrast) self.contrastSlider.valueChanged.connect(self.setContrast) self.hueSlider = QSlider(Qt.Horizontal) self.hueSlider.setRange(-100, 100) self.hueSlider.setValue(self.mediaPlayer.hue) self.hueSlider.valueChanged.connect(self.setHue) self.saturationSlider = QSlider(Qt.Horizontal) self.saturationSlider.setRange(-100, 100) self.saturationSlider.setValue(self.mediaPlayer.saturation) self.saturationSlider.valueChanged.connect(self.setSaturation) layout = QFormLayout() layout.addRow("Brightness", self.brightnessSlider) layout.addRow("Contrast", self.contrastSlider) layout.addRow("Hue", self.hueSlider) layout.addRow("Color", self.saturationSlider) btn = QPushButton("Reset") btn.setIcon(QIcon.fromTheme("preferences-color")) layout.addRow(btn) button = QPushButton("Close") button.setIcon(QIcon.fromTheme("ok")) layout.addRow(button) self.colorDialog = QDialog(self) self.colorDialog.setWindowTitle("Color Settings") self.colorDialog.setLayout(layout) btn.clicked.connect(self.resetColors) button.clicked.connect(self.colorDialog.close) self.colorDialog.resize(300, 180) self.colorDialog.show() def resetColors(self): self.brightnessSlider.setValue(0) self.mediaPlayer.brightness = (0) self.contrastSlider.setValue(0) self.mediaPlayer.contrast = (0) self.saturationSlider.setValue(0) self.mediaPlayer.saturation = (0) self.hueSlider.setValue(0) self.mediaPlayer.hue = (0)
class PlainTextEdit(QPlainTextEdit): commandSignal = pyqtSignal(str) commandZPressed = pyqtSignal(str) startDir = '' def __init__(self, parent=None, movable=False): super(PlainTextEdit, self).__init__() self.installEventFilter(self) self.setAcceptDrops(True) QApplication.setCursorFlashTime(1000) self.process = QProcess() self.process.readyReadStandardError.connect(self.onReadyReadStandardError) self.process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput) # global self.startDir(str) self.commands = [] # This is a list to track what commands the user has used so we could display them when # up arrow key is pressed self.tracker = 0 self.setStyleSheet("QPlainTextEdit{background-color: #212121; color: #f3f3f3; padding: 8;}") self.verticalScrollBar().setStyleSheet("background-color: #212121;") self.text = None self.setFont(QFont("Noto Sans Mono", 8)) self.previousCommandLength = 0 self.copySelectedTextAction = QAction(QIcon.fromTheme("edit-copy"), "Copy", shortcut = "Shift+Ctrl+c", triggered = self.copyText) self.addAction(self.copySelectedTextAction) self.pasteTextAction = QAction(QIcon.fromTheme("edit-paste"), "Copy", shortcut = "Shift+Ctrl+v", triggered = self.pasteText) self.addAction(self.pasteTextAction) if not self.startDir == "": os.chdir(self.startDir) self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.appendPlainText(self.name) else: os.chdir(os.path.dirname(sys.argv[0])) self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.appendPlainText(self.name) def copyText(self): self.copy() def pasteText(self): self.paste() def eventFilter(self, source, event): if (event.type() == QEvent.DragEnter): event.accept() print ('DragEnter') return True elif (event.type() == QEvent.Drop): print ('Drop') self.setDropEvent(event) return True else: return False ### super(QPlainTextEdit).eventFilter(event) def setDropEvent(self, event): if event.mimeData().hasUrls(): f = str(event.mimeData().urls()[0].toLocalFile()) self.insertPlainText(f) event.accept() elif event.mimeData().hasText(): ft = event.mimeData().text() print("text:", ft) self.insertPlainText(ft) event.accept() else: event.ignore() def keyPressEvent(self, e): cursor = self.textCursor() if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_A: return if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_Z: self.commandZPressed.emit("True") return if e.modifiers() == Qt.ControlModifier and e.key() == Qt.Key_C: self.process.kill() self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.appendPlainText("process cancelled") self.appendPlainText(self.name) self.textCursor().movePosition(QTextCursor.End) return if e.key() == Qt.Key_Return: text = self.textCursor().block().text() if text == self.name + text.replace(self.name, "") and text.replace(self.name, "") != "": # This is to prevent adding in commands that were not meant to be added in self.commands.append(text.replace(self.name, "")) self.handle(text) self.commandSignal.emit(text) self.appendPlainText(self.name) return if e.key() == Qt.Key_Up: try: if self.tracker != 0: cursor.select(QTextCursor.BlockUnderCursor) cursor.removeSelectedText() self.appendPlainText(self.name) self.insertPlainText(self.commands[self.tracker]) self.tracker -= 1 except IndexError: self.tracker = 0 return if e.key() == Qt.Key_Down: try: cursor.select(QTextCursor.BlockUnderCursor) cursor.removeSelectedText() self.appendPlainText(self.name) self.insertPlainText(self.commands[self.tracker]) self.tracker += 1 except IndexError: self.tracker = 0 if e.key() == Qt.Key_Backspace: if cursor.positionInBlock() <= len(self.name): return if e.key() == Qt.Key_Left: if cursor.positionInBlock() <= len(self.name): return if e.key() == Qt.Key_Delete: return super().keyPressEvent(e) cursor = self.textCursor() e.accept() def ispressed(self): return self.pressed def onReadyReadStandardError(self): self.error = self.process.readAllStandardError().data().decode() self.appendPlainText(self.error.strip('\n')) def onReadyReadStandardOutput(self): self.result = self.process.readAllStandardOutput().data().decode() self.appendPlainText(self.result.strip('\n')) self.state = self.process.state() def run(self, command): """Executes a system command.""" if not command == "ls": if self.process.state() != 2: self.process.start(command) print(self.process.exitStatus()) if not self.process.exitStatus() != 0: self.textCursor().movePosition(QTextCursor.End) self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.appendPlainText(self.name) else: if self.process.state() != 2: self.process.start(command) self.process.waitForFinished() def handle(self, command): # print("begin handle") """Split a command into list so command echo hi would appear as ['echo', 'hi']""" real_command = command.replace(self.name, "") if command == "True": if self.process.state() == 2: self.process.kill() self.appendPlainText("Program execution killed, press enter") if real_command.startswith("python"): pass if real_command != "": command_list = real_command.split() else: command_list = None """Now we start implementing some commands""" if real_command == "clear": self.clear() elif command_list is not None and command_list[0] == "echo": self.appendPlainText(" ".join(command_list[1:])) elif real_command == "exit": quit() elif command_list is not None and command_list[0] == "cd" and len(command_list) > 1: try: os.chdir(" ".join(command_list[1:])) self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.textCursor().movePosition(QTextCursor.End) except FileNotFoundError as E: self.appendPlainText(str(E)) elif command_list is not None and len(command_list) == 1 and command_list[0] == "cd": os.chdir(str(Path.home())) self.name = (str(getpass.getuser()) + "@" + str(socket.gethostname()) + ":" + str(os.getcwd()) + "$ ") self.textCursor().movePosition(QTextCursor.End) elif self.process.state() == 2: self.process.write(real_command.encode()) self.process.closeWriteChannel() elif command == self.name + real_command: self.run(real_command) else: pass
class Process(QObject): """ Use the QProcess mechanism to run a subprocess asynchronously This will interact well with Qt Gui objects, eg by connecting the `output` signals to an `QTextEdit.append` method and the `started` and `finished` signals to a `QPushButton.setEnabled`. eg:: import sys from PyQt5.QtCore import * from PyQt5.QtWidgets import * class Example(QMainWindow): def __init__(self): super().__init__() textEdit = QTextEdit() self.setCentralWidget(textEdit) self.setGeometry(300, 300, 350, 250) self.setWindowTitle('Main window') self.show() self.process = Process() self.process.output.connect(textEdit.append) self.process.run(sys.executable, ["-u", "-m", "pip", "list"]) def main(): app = QApplication(sys.argv) ex = Example() sys.exit(app.exec_()) """ started = pyqtSignal() output = pyqtSignal(str) finished = pyqtSignal() Slots = namedtuple("Slots", ["started", "output", "finished"]) Slots.__new__.__defaults__ = (None, None, None) def __init__(self): super().__init__() # # Always run unbuffered and with UTF-8 IO encoding # self.environment = QProcessEnvironment.systemEnvironment() self.environment.insert("PYTHONUNBUFFERED", "1") self.environment.insert("PYTHONIOENCODING", "utf-8") def _set_up_run(self, **envvars): """Set up common elements of a QProcess run""" self.process = QProcess() environment = QProcessEnvironment(self.environment) for k, v in envvars.items(): environment.insert(k, v) self.process.setProcessEnvironment(environment) self.process.setProcessChannelMode(QProcess.MergedChannels) def run_blocking(self, command, args, wait_for_s=30.0, **envvars): """Run `command` with `args` via QProcess, passing `envvars` as environment variables for the process. Wait `wait_for_s` seconds for completion and return any stdout/stderr """ logger.info( "About to run blocking %s with args %s and envvars %s", command, args, envvars, ) self._set_up_run(**envvars) self.process.start(command, args) return self.wait(wait_for_s=wait_for_s) def run(self, command, args, **envvars): """Run `command` asynchronously with `args` via QProcess, passing `envvars` as environment variables for the process.""" logger.info( "About to run %s with args %s and envvars %s", command, args, envvars, ) self._set_up_run(**envvars) self.process.readyRead.connect(self._readyRead) self.process.started.connect(self._started) self.process.finished.connect(self._finished) partial = functools.partial(self.process.start, command, args) QTimer.singleShot(1, partial) def wait(self, wait_for_s=30): """Wait for the process to complete, optionally timing out. Return any stdout/stderr. If the process fails to complete in time or returns an error, raise a VirtualEnvironmentError """ finished = self.process.waitForFinished(int(1000 * wait_for_s)) exit_status = self.process.exitStatus() exit_code = self.process.exitCode() output = self.data() # # if waitForFinished completes, either the process has successfully finished # or it crashed, was terminated or timed out. If it does finish successfully # we might still have an error code. In each case we might still have data # from stdout/stderr. Unfortunately there's no way to determine that the # process was timed out, as opposed to crashing in some other way # # The three elements in play are: # # finished (yes/no) # exitStatus (normal (0) / crashed (1)) -- we don't currently distinguish # exitCode (whatever the program returns; conventionally 0 => ok) # logger.debug( "Finished: %s; exitStatus %s; exitCode %s", finished, exit_status, exit_code, ) # # Exceptions raised here will be caught by the crash-handler which will try to # generate a URI out of it. There's an upper limit on URI size of ~2000 # if not finished: logger.error(compact(output)) raise VirtualEnvironmentError( "Process did not terminate normally:\n" + compact(output)) if exit_code != 0: # # We finished normally but we might still have an error-code on finish # logger.error(compact(output)) raise VirtualEnvironmentError( "Process finished but with error code %d:\n%s" % (exit_code, compact(output))) return output def data(self): """Return all the data from the running process, converted to unicode""" output = self.process.readAll().data() return output.decode(ENCODING, errors="replace") def _started(self): self.started.emit() def _readyRead(self): self.output.emit(self.data().strip()) def _finished(self): self.finished.emit()
class TVLinker(QWidget): def __init__(self, settings: QSettings, parent=None): super(TVLinker, self).__init__(parent) self.firstrun = True self.rows, self.cols = 0, 0 self.parent = parent self.settings = settings self.taskbar = TaskbarProgress(self) self.init_styles() self.init_settings() self.init_icons() if sys.platform.startswith('linux'): notify.init(qApp.applicationName()) layout = QVBoxLayout() layout.setSpacing(0) layout.setContentsMargins(15, 15, 15, 0) form_groupbox = QGroupBox(self, objectName='mainForm') form_groupbox.setLayout(self.init_form()) self.table = TVLinkerTable(0, 4, self) self.table.doubleClicked.connect(self.show_hosters) layout.addWidget(form_groupbox) layout.addWidget(self.table) layout.addLayout(self.init_metabar()) self.setLayout(layout) qApp.setWindowIcon(self.icon_app) self.resize(FixedSettings.windowSize) self.show() self.start_scraping() self.firstrun = False class ProcError(Enum): FAILED_TO_START = 0 CRASHED = 1 TIMED_OUT = 2 READ_ERROR = 3 WRITE_ERROR = 4 UNKNOWN_ERROR = 5 class NotifyIcon(Enum): SUCCESS = ':assets/images/tvlinker.png' DEFAULT = ':assets/images/tvlinker.png' def init_threads(self, threadtype: str = 'scrape') -> None: if threadtype == 'scrape': if hasattr(self, 'scrapeThread'): if not sip.isdeleted( self.scrapeThread) and self.scrapeThread.isRunning(): self.scrapeThread.terminate() del self.scrapeWorker del self.scrapeThread self.scrapeThread = QThread(self) self.scrapeWorker = ScrapeWorker(self.source_url, self.user_agent, self.dl_pagecount) self.scrapeThread.started.connect(self.show_progress) self.scrapeThread.started.connect(self.scrapeWorker.begin) self.scrapeWorker.moveToThread(self.scrapeThread) self.scrapeWorker.addRow.connect(self.add_row) self.scrapeWorker.workFinished.connect(self.scrape_finished) self.scrapeWorker.workFinished.connect( self.scrapeWorker.deleteLater, Qt.DirectConnection) self.scrapeWorker.workFinished.connect(self.scrapeThread.quit, Qt.DirectConnection) self.scrapeThread.finished.connect(self.scrapeThread.deleteLater, Qt.DirectConnection) elif threadtype == 'unrestrict': pass @staticmethod def load_stylesheet(qssfile: str) -> None: if QFileInfo(qssfile).exists(): qss = QFile(qssfile) qss.open(QFile.ReadOnly | QFile.Text) qApp.setStyleSheet(QTextStream(qss).readAll()) def init_styles(self) -> None: if sys.platform == 'darwin': qss_stylesheet = self.get_path('%s_osx.qss' % qApp.applicationName().lower()) else: qss_stylesheet = self.get_path('%s.qss' % qApp.applicationName().lower()) TVLinker.load_stylesheet(qss_stylesheet) QFontDatabase.addApplicationFont(':assets/fonts/opensans.ttf') QFontDatabase.addApplicationFont(':assets/fonts/opensans-bold.ttf') QFontDatabase.addApplicationFont(':assets/fonts/opensans-semibold.ttf') qApp.setFont(QFont('Open Sans', 12 if sys.platform == 'darwin' else 10)) def init_icons(self) -> None: self.icon_app = QIcon( self.get_path('images/%s.png' % qApp.applicationName().lower())) self.icon_faves_off = QIcon(':assets/images/star_off.png') self.icon_faves_on = QIcon(':assets/images/star_on.png') self.icon_refresh = QIcon(':assets/images/refresh.png') self.icon_menu = QIcon(':assets/images/menu.png') self.icon_settings = QIcon(':assets/images/cog.png') self.icon_updates = QIcon(':assets/images/cloud.png') def init_settings(self) -> None: self.provider = 'Scene-RLS' self.select_provider(0) self.user_agent = self.settings.value('user_agent') self.dl_pagecount = self.settings.value('dl_pagecount', 20, int) self.dl_pagelinks = FixedSettings.linksPerPage self.realdebrid_api_token = self.settings.value('realdebrid_apitoken') self.realdebrid_api_proxy = self.settings.value('realdebrid_apiproxy') self.download_manager = self.settings.value('download_manager') self.persepolis_cmd = self.settings.value('persepolis_cmd') self.pyload_host = self.settings.value('pyload_host') self.pyload_username = self.settings.value('pyload_username') self.pyload_password = self.settings.value('pyload_password') self.idm_exe_path = self.settings.value('idm_exe_path') self.kget_cmd = self.settings.value('kget_cmd') self.favorites = self.settings.value('favorites') def init_form(self) -> QHBoxLayout: self.search_field = QLineEdit(self, clearButtonEnabled=True, placeholderText='Enter search criteria') self.search_field.setObjectName('searchInput') self.search_field.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) self.search_field.setFocus() self.search_field.textChanged.connect(self.clear_filters) self.search_field.returnPressed.connect( lambda: self.filter_table(self.search_field.text())) self.favorites_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, objectName='favesButton', toolTip='Favorites', checkable=True, toggled=self.filter_faves, checked=self.settings.value( 'faves_filter', False, bool)) self.refresh_button = QPushButton(parent=self, flat=True, cursor=Qt.PointingHandCursor, objectName='refreshButton', toolTip='Refresh', clicked=self.start_scraping) self.dlpages_field = QComboBox(self, toolTip='Pages', editable=False, cursor=Qt.PointingHandCursor) self.dlpages_field.addItems( ('10', '20', '30', '40', '50', '60', '70', '80')) self.dlpages_field.setCurrentIndex( self.dlpages_field.findText(str(self.dl_pagecount), Qt.MatchFixedString)) self.dlpages_field.currentIndexChanged.connect(self.update_pagecount) self.settings_button = QPushButton(parent=self, flat=True, toolTip='Menu', objectName='menuButton', cursor=Qt.PointingHandCursor) self.settings_button.setMenu(self.settings_menu()) layout = QHBoxLayout(spacing=10) # providerCombo = QComboBox(self, toolTip='Provider', editable=False, cursor=Qt.PointingHandCursor) # providerCombo.setObjectName('providercombo') # providerCombo.addItem(QIcon(':assets/images/provider-scenerls.png'), '') # providerCombo.addItem(QIcon(':assets/images/provider-tvrelease.png'), '') # providerCombo.setIconSize(QSize(146, 36)) # providerCombo.setMinimumSize(QSize(160, 40)) # providerCombo.setStyleSheet(''' # QComboBox, QComboBox::drop-down { background-color: transparent; border: none; margin: 5px; } # QComboBox::down-arrow { image: url(:assets/images/down_arrow.png); } # QComboBox QAbstractItemView { selection-background-color: #DDDDE4; } # ''') # providerCombo.currentIndexChanged.connect(self.select_provider) layout.addWidget( QLabel(pixmap=QPixmap(':assets/images/provider-scenerls.png'))) layout.addWidget(self.search_field) layout.addWidget(self.favorites_button) layout.addWidget(self.refresh_button) layout.addWidget(QLabel('Pages:')) layout.addWidget(self.dlpages_field) layout.addWidget(self.settings_button) return layout @pyqtSlot(int) def select_provider(self, index: int): if index == 0: self.provider = 'Scene-RLS' self.source_url = 'http://scene-rls.net/releases/index.php?p={0}&cat=TV%20Shows' elif index == 1: self.provider = 'TV-Release' self.source_url = 'http://tv-release.pw/?cat=TV' self.setWindowTitle('%s :: %s' % (qApp.applicationName(), self.provider)) def settings_menu(self) -> QMenu: settings_action = QAction(self.icon_settings, 'Settings', self, triggered=self.show_settings) updates_action = QAction(self.icon_updates, 'Check for updates', self, triggered=self.check_update) aboutqt_action = QAction('About Qt', self, triggered=qApp.aboutQt) about_action = QAction('About %s' % qApp.applicationName(), self, triggered=self.about_app) menu = QMenu() menu.addAction(settings_action) menu.addAction(updates_action) menu.addSeparator() menu.addAction(aboutqt_action) menu.addAction(about_action) return menu def init_metabar(self) -> QHBoxLayout: self.meta_template = 'Total number of links retrieved: <b>%i</b> / <b>%i</b>' self.progress = QProgressBar(parent=self, minimum=0, maximum=(self.dl_pagecount * self.dl_pagelinks), visible=False) self.taskbar.setProgress(0.0, True) if sys.platform == 'win32': self.win_taskbar_button = QWinTaskbarButton(self) self.meta_label = QLabel(textFormat=Qt.RichText, alignment=Qt.AlignRight, objectName='totals') self.update_metabar() layout = QHBoxLayout() layout.setContentsMargins(10, 5, 10, 10) layout.addWidget(self.progress, Qt.AlignLeft) layout.addWidget(self.meta_label, Qt.AlignRight) return layout @pyqtSlot() def check_update(self) -> None: QDesktopServices.openUrl(QUrl(FixedSettings.latest_release_url)) @pyqtSlot() def show_settings(self) -> None: settings_win = Settings(self, self.settings) settings_win.exec_() def update_metabar(self) -> bool: rowcount = self.table.rowCount() self.meta_label.setText( self.meta_template % (rowcount, self.dl_pagecount * self.dl_pagelinks)) self.progress.setValue(rowcount) self.taskbar.setProgress(rowcount / self.progress.maximum()) if sys.platform == 'win32': self.win_taskbar_button.progress().setValue(self.progress.value()) return True def start_scraping(self) -> None: self.init_threads('scrape') self.rows = 0 self.table.clearContents() self.table.setRowCount(0) self.table.setSortingEnabled(False) self.update_metabar() self.scrapeThread.start() @pyqtSlot() def about_app(self) -> None: about_html = '''<style> a { color:#441d4e; text-decoration:none; font-weight:bold; } a:hover { text-decoration:underline; } </style> <p style="font-size:24pt; font-weight:bold; color:#6A687D;">%s</p> <p> <span style="font-size:13pt;"><b>Version: %s</b></span> <span style="font-size:10pt;position:relative;left:5px;">( %s )</span> </p> <p style="font-size:13px;"> Copyright © %s <a href="mailto:[email protected]">Pete Alexandrou</a> <br/> Web: <a href="%s">%s</a> </p> <p style="font-size:11px;"> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. </p>''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], datetime.now().year, qApp.organizationDomain(), qApp.organizationDomain()) QMessageBox.about(self, 'About %s' % qApp.applicationName(), about_html) @pyqtSlot(int) def update_pagecount(self, index: int) -> None: self.dl_pagecount = int(self.dlpages_field.itemText(index)) self.scrapeWorker.maxpages = self.dl_pagecount self.progress.setMaximum(self.dl_pagecount * self.dl_pagelinks) self.settings.setValue('dl_pagecount', self.dl_pagecount) if sys.platform == 'win32': self.win_taskbar_button.progress().setMaximum(self.dl_pagecount * self.dl_pagelinks) if self.scrapeThread.isRunning(): self.scrapeThread.requestInterruption() self.start_scraping() @pyqtSlot() def show_progress(self): self.progress.show() self.taskbar.setProgress(0.0, True) if sys.platform == 'win32': self.win_taskbar_button.setWindow(self.windowHandle()) self.win_taskbar_button.progress().setRange( 0, self.dl_pagecount * self.dl_pagelinks) self.win_taskbar_button.progress().setVisible(True) self.win_taskbar_button.progress().setValue(self.progress.value()) @pyqtSlot() def scrape_finished(self) -> None: self.progress.hide() self.taskbar.setProgress(0.0, False) if sys.platform == 'win32': self.win_taskbar_button.progress().setVisible(False) self.table.setSortingEnabled(True) self.filter_table(text='') @pyqtSlot(list) def add_row(self, row: list) -> None: if self.scrapeThread.isInterruptionRequested(): self.scrapeThread.terminate() else: self.cols = 0 self.table.setRowCount(self.rows + 1) if self.table.cursor() != Qt.PointingHandCursor: self.table.setCursor(Qt.PointingHandCursor) for item in row: table_item = QTableWidgetItem(item) table_item.setToolTip( '%s\n\nDouble-click to view hoster links.' % row[1]) table_item.setFont(QFont('Open Sans', weight=QFont.Normal)) if self.cols == 2: if sys.platform == 'win32': table_item.setFont( QFont('Open Sans Semibold', pointSize=10)) elif sys.platform == 'darwin': table_item.setFont( QFont('Open Sans Bold', weight=QFont.Bold)) else: table_item.setFont( QFont('Open Sans', weight=QFont.DemiBold, pointSize=10)) table_item.setText(' ' + table_item.text()) elif self.cols in (0, 3): table_item.setTextAlignment(Qt.AlignCenter) self.table.setItem(self.rows, self.cols, table_item) self.update_metabar() self.cols += 1 self.rows += 1 @pyqtSlot(list) def add_hosters(self, links: list) -> None: self.hosters_win.show_hosters(links) @pyqtSlot(QModelIndex) def show_hosters(self, index: QModelIndex) -> None: qApp.setOverrideCursor(Qt.BusyCursor) self.hosters_win = HosterLinks(self) self.hosters_win.downloadLink.connect(self.download_link) self.hosters_win.copyLink.connect(self.copy_download_link) self.links = HostersThread( self.table.item(self.table.currentRow(), 1).text(), self.user_agent) self.links.setHosters.connect(self.add_hosters) self.links.noLinks.connect(self.no_links) self.links.start() @pyqtSlot() def no_links(self) -> None: self.hosters_win.loading_progress.cancel() self.hosters_win.close() QMessageBox.warning( self, 'No Links Available', 'No links are available yet for the chosen TV show. ' + 'This is most likely due to the files still being uploaded. This is normal if the ' + 'link was published 30-45 mins ago.\n\nPlease check back again in 10-15 minutes.' ) @pyqtSlot(bool) def filter_faves(self, checked: bool) -> None: self.settings.setValue('faves_filter', checked) # if hasattr(self, 'scrapeWorker') and (sip.isdeleted(self.scrapeWorker) or self.scrapeWorker.complete): if not self.firstrun: self.filter_table() @pyqtSlot(str) @pyqtSlot() def filter_table(self, text: str = '') -> None: filters = [] if self.favorites_button.isChecked(): filters = self.favorites self.table.sortItems(2, Qt.AscendingOrder) else: self.table.sortItems(0, Qt.DescendingOrder) if len(text): filters.append(text) if not len(filters) or not hasattr(self, 'valid_rows'): self.valid_rows = [] for search_term in filters: for item in self.table.findItems(search_term, Qt.MatchContains): self.valid_rows.append(item.row()) for row in range(0, self.table.rowCount()): if not len(filters): self.table.showRow(row) else: if row not in self.valid_rows: self.table.hideRow(row) else: self.table.showRow(row) @pyqtSlot() def clear_filters(self): if not len(self.search_field.text()): self.filter_table('') @pyqtSlot(bool) def aria2_confirmation(self, success: bool) -> None: qApp.restoreOverrideCursor() if success: if sys.platform.startswith('linux'): self.notify( title=qApp.applicationName(), msg='Your download link has been unrestricted and now ' + 'queued in Aria2 RPC Daemon', icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, qApp.applicationName(), 'Download link has been queued in Aria2.', QMessageBox.Ok) else: QMessageBox.critical( self, 'Aria2 RPC Daemon', 'Could not connect to Aria2 RPC Daemon. ' + 'Check your %s settings and try again.' % qApp.applicationName(), QMessageBox.Ok) @pyqtSlot(str) def download_link(self, link: str) -> None: if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ and 'rdeb.io' not in link: qApp.setOverrideCursor(Qt.BusyCursor) self.unrestrict_link(link, True) else: if self.download_manager == 'aria2': self.aria2 = Aria2Thread(settings=self.settings, link_url=link) self.aria2.aria2Confirmation.connect(self.aria2_confirmation) self.aria2.start() self.hosters_win.close() elif self.download_manager == 'pyload': self.pyload_conn = PyloadConnection(self.pyload_host, self.pyload_username, self.pyload_password) pid = self.pyload_conn.addPackage(name='TVLinker', links=[link]) qApp.restoreOverrideCursor() self.hosters_win.close() if sys.platform.startswith('linux'): self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, self.download_manager, 'Your link has been queued in %s.' % self.download_manager, QMessageBox.Ok) # open_pyload = msgbox.addButton('Open pyLoad', QMessageBox.AcceptRole) # open_pyload.clicked.connect(self.open_pyload) elif self.download_manager in ('kget', 'persepolis'): provider = self.kget_cmd if self.download_manager == 'kget' else self.persepolis_cmd cmd = '{0} "{1}"'.format(provider, link) if self.cmdexec(cmd): qApp.restoreOverrideCursor() self.hosters_win.close() if sys.platform.startswith('linux'): self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) else: QMessageBox.information( self, self.download_manager, 'Your link has been queued in %s.' % self.download_manager, QMessageBox.Ok) elif self.download_manager == 'idm': cmd = '"%s" /n /d "%s"' % (self.idm_exe_path, link) if self.cmdexec(cmd): qApp.restoreOverrideCursor() self.hosters_win.close() QMessageBox.information( self, 'Internet Download Manager', 'Your link has been queued in IDM.') else: print('IDM QProcess error = %s' % self.ProcError(self.idm.error()).name) qApp.restoreOverrideCursor() self.hosters_win.close() QMessageBox.critical( self, 'Internet Download Manager', '<p>Could not connect to your local IDM application instance. ' + 'Please check your settings and ensure the IDM executable path is correct ' + 'according to your installation.</p><p>Error Code: %s</p>' % self.ProcError(self.idm.error()).name, QMessageBox.Ok) else: dlpath, _ = QFileDialog.getSaveFileName( self, 'Save File', link.split('/')[-1]) if dlpath != '': self.directdl_win = DirectDownload(parent=self) self.directdl = DownloadThread(link_url=link, dl_path=dlpath) self.directdl.dlComplete.connect( self.directdl_win.download_complete) if sys.platform.startswith('linux'): self.directdl.dlComplete.connect( lambda: self.notify(qApp.applicationName( ), 'Download complete', self.NotifyIcon.SUCCESS)) else: self.directdl.dlComplete.connect( lambda: QMessageBox.information( self, qApp.applicationName(), 'Download complete', QMessageBox.Ok)) self.directdl.dlProgressTxt.connect( self.directdl_win.update_progress_label) self.directdl.dlProgress.connect( self.directdl_win.update_progress) self.directdl_win.cancelDownload.connect( self.cancel_download) self.directdl.start() self.hosters_win.close() def _init_notification_icons(self): for icon in self.NotifyIcon: icon_file = QPixmap(icon.value, 'PNG') icon_file.save( os.path.join(FixedSettings.config_path, os.path.basename(icon.value)), 'PNG', 100) def notify(self, title: str, msg: str = '', icon: Enum = None, urgency: int = 1) -> bool: icon_path = icon.value if icon is not None else self.NotifyIcon.DEFAULT.value icon_path = os.path.join(FixedSettings.config_path, os.path.basename(icon_path)) if not os.path.exists(icon_path): self._init_notification_icons() notification = notify.Notification(title, msg, icon_path) notification.set_urgency(urgency) return notification.show() def cmdexec(self, cmd: str) -> bool: self.proc = QProcess() self.proc.setProcessChannelMode(QProcess.MergedChannels) if hasattr(self.proc, 'errorOccurred'): self.proc.errorOccurred.connect(lambda error: print( 'Process error = %s' % self.ProcError(error).name)) if self.proc.state() == QProcess.NotRunning: self.proc.start(cmd) self.proc.waitForFinished(-1) rc = self.proc.exitStatus( ) == QProcess.NormalExit and self.proc.exitCode() == 0 self.proc.deleteLater() return rc return False @pyqtSlot() def cancel_download(self) -> None: self.directdl.cancel_download = True self.directdl.quit() self.directdl.deleteLater() def open_pyload(self) -> None: QDesktopServices.openUrl(QUrl(self.pyload_config.host)) @pyqtSlot(str) def copy_download_link(self, link: str) -> None: if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ and 'rdeb.io' not in link: qApp.setOverrideCursor(Qt.BusyCursor) self.unrestrict_link(link, False) else: clip = qApp.clipboard() clip.setText(link) self.hosters_win.close() qApp.restoreOverrideCursor() def unrestrict_link(self, link: str, download: bool = True) -> None: caller = inspect.stack()[1].function self.realdebrid = RealDebridThread( settings=self.settings, api_url=FixedSettings.realdebrid_api_url, link_url=link, action=RealDebridThread.RealDebridAction.UNRESTRICT_LINK) self.realdebrid.errorMsg.connect(self.error_handler) if download: self.realdebrid.unrestrictedLink.connect(self.download_link) else: self.realdebrid.unrestrictedLink.connect(self.copy_download_link) self.realdebrid.start() def closeEvent(self, event: QCloseEvent) -> None: if hasattr(self, 'scrapeThread'): if not sip.isdeleted( self.scrapeThread) and self.scrapeThread.isRunning(): self.scrapeThread.requestInterruption() self.scrapeThread.quit() qApp.quit() def error_handler(self, props: list) -> None: qApp.restoreOverrideCursor() QMessageBox.critical(self, props[0], props[1], QMessageBox.Ok) @staticmethod def get_path(path: str = None, override: bool = False) -> str: if override: if getattr(sys, 'frozen', False): return os.path.join(sys._MEIPASS, path) return os.path.join(QFileInfo(__file__).absolutePath(), path) return ':assets/%s' % path @staticmethod def get_version(filename: str = '__init__.py') -> str: with open(TVLinker.get_path(filename, override=True), 'r') as initfile: for line in initfile.readlines(): m = re.match('__version__ *= *[\'](.*)[\']', line) if m: return m.group(1)
def init_ssr_at(path, signals): root = os.path.join(path, 'qt-ssr') # remove existing folder if os.path.isdir(root): signals.stdout.emit('Removing existing directory %s ... \n' % root) shutil.rmtree(root) # make directory $path/qt-ssr signals.stdout.emit('Making directory %s ...\n' % root) os.mkdir(root) # TODO: acquire version automatically version = '2.5.3' dir_name = 'shadowsocksr-libev-%s' % version url = "https://github.com/shadowsocksrr/shadowsocksr-libev/archive/2.5.3.tar.gz" ssr_dir = os.path.join(root, dir_name) signals.stdout.emit('Downloading and extracting from %s ...\n' % url) with http.request('GET', url, preload_content=False) as resp: with tarfile.open(mode='r:gz', fileobj=resp) as zip_fp: zip_fp.extractall(root, numeric_owner=True) bin_dir = os.path.join(ssr_dir, 'src') bin_path = os.path.join(bin_dir, 'ss-local') # remove -Werrors # TODO: remove shell dependencies os.chdir(bin_dir) p = QProcess() def read_stderr(): qb_arr = p.readAllStandardError() signals.stderr.emit(str(qb_arr.data(), 'utf-8')) def read_stdout(): qb_arr = p.readAllStandardOutput() signals.stdout.emit(str(qb_arr.data(), 'utf-8')) p.readyReadStandardError.connect(read_stderr) p.readyReadStandardOutput.connect(read_stdout) signals.stdout.emit('Removing -Werror from makefiles ...\n') p.start('/usr/bin/bash', ['-c', r"/usr/bin/sed -i 's/-Werror//g' Makefile*"]) p.waitForFinished() if p.exitStatus() != QProcess.NormalExit: raise CalledProcessError(p.exitCode(), p.program()) signals.stdout.emit('Configuring and Making Binaries ... \n') os.chdir(ssr_dir) p.start('/usr/bin/bash', ['-c', r"./configure && make -j$(nproc)"]) p.waitForFinished() if p.exitStatus() != QProcess.NormalExit: raise CalledProcessError(p.exitCode(), p.program()) os.mkdir(os.path.join(root, 'server-configs')) db_path = os.path.join(root, 'server-configs', 'servers.db') signals.stdout.emit('Configuring database...') setting = QSettings() setting.sync() setting.beginGroup('SSR') setting.setValue('root_dir', root) setting.setValue('bin_path', bin_path) setting.setValue('db_path', db_path) setting.endGroup() setting.sync() db_params.path = db_path signals.stdout.emit('Done! \n')
class Updater(QThread): updateAvailable = pyqtSignal(bool, str) pypi_api_endpoint = 'https://pypi.python.org/pypi/vidcutter/json' github_api_endpoint = 'https://api.github.com/repos/ozmartian/vidcutter/releases/latest' latest_release_webpage = 'https://github.com/ozmartian/vidcutter/releases/latest' def __init__(self): QThread.__init__(self) def __del__(self) -> None: self.wait() @staticmethod def restart_app(): os.execl(sys.executable, sys.executable, *sys.argv) def cmd_exec(self, cmd: str, args: str = None) -> bool: self.proc = QProcess(self) self.proc.setProcessChannelMode(QProcess.MergedChannels) self.proc.setWorkingDirectory(QFileInfo(__file__).absolutePath()) if hasattr(self.proc, 'errorOccurred'): self.proc.errorOccurred.connect(self.cmdError) if self.proc.state() == QProcess.NotRunning: self.proc.start(cmd, shlex.split(args)) self.proc.waitForFinished(-1) if self.proc.exitStatus( ) == QProcess.NormalExit and self.proc.exitCode() == 0: return True return False @pyqtSlot(QProcess.ProcessError) def cmdError(self, error: QProcess.ProcessError) -> None: if error != QProcess.Crashed: QMessageBox.critical(None, "Error calling an external process", self.proc.errorString(), buttons=QMessageBox.Close) def check_latest_github(self) -> None: try: res = json.loads( urlopen(self.github_api_endpoint).read().decode('utf-8')) if 'tag_name' in res.keys(): latest_release = str(res['tag_name']) if LooseVersion(latest_release) > LooseVersion( qApp.applicationVersion()): # self.notify_update(version=latest_release, installer=self.install_update) self.updateAvailable.emit(True, latest_release) return self.updateAvailable.emit(False, None) except HTTPError: self.updateAvailable.emit(False, None) def check_latest_pypi(self) -> None: try: res = json.loads( urlopen(self.pypi_api_endpoint).read().decode('utf-8')) if 'info' in res.keys(): latest_release = str(res['info']['version']) if LooseVersion(latest_release) > LooseVersion( qApp.applicationVersion()): # self.notify_update(version=latest_release, installer=self.install_update) self.updateAvailable.emit(True, latest_release) return self.updateAvailable.emit(False, None) except HTTPError: self.updateAvailable.emit(False, None) def install_update(self, parent: QWidget) -> None: returncode = self.cmd_exec( 'x-terminal-emulator', '-title "VidCutter Updater" -e "sudo pip3 install ' + '--upgrade vidcutter"') self.confirm_update(parent, returncode) def run(self) -> None: if getattr(sys, 'frozen', False): self.check_latest_github() else: self.check_latest_pypi() @staticmethod def notify_update(parent: QWidget, version: str) -> QMessageBox.ButtonRole: mbox = QMessageBox(parent) mbox.setIconPixmap(qApp.windowIcon().pixmap(64, 64)) mbox.setWindowTitle('%s UPDATER' % qApp.applicationName()) mbox.setText( '<table border="0" width="350"><tr><td><h4 align="center">Your Version: %s <br/> Available Version: %s' % (qApp.applicationVersion(), version) + '</h4></td></tr></table><br/>') mbox.setInformativeText( 'A new version of %s has been detected. Would you like to update now?' % qApp.applicationName()) install_btn = mbox.addButton('Install Update', QMessageBox.AcceptRole) reject_btn = mbox.addButton('Not Now', QMessageBox.RejectRole) mbox.setDefaultButton(install_btn) return mbox.exec_() @staticmethod def notify_no_update(parent: QWidget) -> None: mbox = QMessageBox(parent) mbox.setIconPixmap(QIcon(':/images/thumbsup.png').pixmap(64, 64)) mbox.setWindowTitle('%s UPDATER' % qApp.applicationName()) mbox.setText('<h3 style="color:#6A4572;">%s %s</h3>' % (qApp.applicationName(), qApp.applicationVersion())) mbox.setInformativeText( 'You are already running the latest version.' + '<table width="350"><tr><td></td></tr></table>') mbox.setStandardButtons(QMessageBox.Close) mbox.setDefaultButton(QMessageBox.Close) return mbox.exec_() @staticmethod def notify_restart(parent: QWidget) -> bool: mbox = QMessageBox(parent) mbox.setIconPixmap(qApp.windowIcon().pixmap(64, 64)) mbox.setWindowTitle('%s UPDATER' % qApp.applicationName()) mbox.setText( '<h3 style="color:#6A4572;">INSTALLATION COMPLETE</h3>' + '<table border="0" width="350"><tr><td><p>The application needs to be restarted in order to use ' + 'the newly installed version.</p><p>Would you like to restart now?</td></tr></table><br/>' ) restart_btn = mbox.addButton('Yes', QMessageBox.AcceptRole) restart_btn.clicked.connect(Updater.restart_app) reject_btn = mbox.addButton('No', QMessageBox.RejectRole) mbox.setDefaultButton(restart_btn) return mbox.exec_() @staticmethod def confirm_update(parent: QWidget, update_success: bool) -> None: if update_success and QMessageBox.question( parent, '%s UPDATER' % qApp.applicationName(), '<h3>UPDATE COMPLETE</h3><p>To begin using the newly installed ' + 'version the application needs to be restarted.</p>' + '<p>Would you like to restart now?</p><br/>', buttons=(QMessageBox.Yes | QMessageBox.No)): Updater.restart_app()
class QuantyDockWidget(QDockWidget): _defaults = { 'element': 'Ni', 'charge': '2+', 'symmetry': 'Oh', 'experiment': 'XAS', 'edge': 'L2,3 (2p)', 'temperature': 10.0, 'magneticField': (0.0, 0.0, 0.0), 'shells': None, 'nPsis': None, 'energies': None, 'hamiltonianParameters': None, 'hamiltonianTermsCheckState': None, 'spectra': None, 'templateName': None, 'baseName': 'untitled', 'label': None, 'uuid': None, 'startingTime': None, 'endingTime': None, } def __init__(self): super(QuantyDockWidget, self).__init__() self.__dict__.update(self._defaults) # Load the external .ui file for the widget. path = resourceFileName(os.path.join('gui', 'uis', 'quanty.ui')) loadUi(path, baseinstance=self, package='crispy.gui') # Remove macOS focus border. for child in self.findChildren((QListView, TreeView, QDoubleSpinBox)): child.setAttribute(Qt.WA_MacShowFocusRect, False) self.setParameters() self.activateUi() def setParameters(self): # Load the external parameters. path = resourceFileName( os.path.join('modules', 'quanty', 'parameters', 'ui.json')) with open(path) as p: uiParameters = json.loads( p.read(), object_pairs_hook=collections.OrderedDict) self.elements = uiParameters['elements'] if self.element not in self.elements: self.element = tuple(self.elements)[0] self.charges = self.elements[self.element]['charges'] if self.charge not in self.charges: self.charge = tuple(self.charges)[0] self.symmetries = self.charges[self.charge]['symmetries'] if self.symmetry not in self.symmetries: self.symmetry = tuple(self.symmetries)[0] self.experiments = self.symmetries[self.symmetry]['experiments'] if self.experiment not in self.experiments: self.experiment = tuple(self.experiments)[0] self.edges = self.experiments[self.experiment]['edges'] if self.edge not in self.edges: self.edge = tuple(self.edges)[0] branch = self.edges[self.edge] self.shells = branch['shells'] self.nPsis = branch['nPsis'] self.energies = branch['energies'] self.templateName = branch['template name'] self.hamiltonians = branch['configurations'] path = resourceFileName( os.path.join('modules', 'quanty', 'parameters', 'hamiltonian.json')) with open(path) as p: hamiltonianParameters = json.loads( p.read(), object_pairs_hook=collections.OrderedDict) self.hamiltonianParameters = OrderedDict() self.hamiltonianTermsCheckState = collections.OrderedDict() for hamiltonian in self.hamiltonians: label = '{} Hamiltonian'.format(hamiltonian[0]) configuration = hamiltonian[1] terms = (hamiltonianParameters['elements'][ self.element]['charges'][self.charge]['configurations'] [configuration]['Hamiltonian Terms']) for term in terms: if ('Coulomb' in term) or ('Spin-Orbit Coupling' in term): parameters = terms[term] else: try: parameters = terms[term][self.symmetry] except KeyError: continue for parameter in parameters: if parameter[0] in ('F', 'G'): scaling = 0.8 else: scaling = 1.0 self.hamiltonianParameters[term][label][parameter] = ( parameters[parameter], scaling) if 'Hybridization' in term: self.hamiltonianTermsCheckState[term] = 0 else: self.hamiltonianTermsCheckState[term] = 2 self.setUi() def updateParameters(self): self.element = self.elementComboBox.currentText() self.charge = self.chargeComboBox.currentText() self.symmetry = self.symmetryComboBox.currentText() self.experiment = self.experimentComboBox.currentText() self.edge = self.edgeComboBox.currentText() self.baseName = self._defaults['baseName'] self.updateMainWindowTitle() self.setParameters() def setUi(self): # Set the values for the combo boxes. self.elementComboBox.setItems(self.elements, self.element) self.chargeComboBox.setItems(self.charges, self.charge) self.symmetryComboBox.setItems(self.symmetries, self.symmetry) self.experimentComboBox.setItems(self.experiments, self.experiment) self.edgeComboBox.setItems(self.edges, self.edge) # Set the temperature spin box. self.temperatureDoubleSpinBox.setValue(self.temperature) # Set the magnetic field spin boxes. self.magneticFieldXDoubleSpinBox.setValue(self.magneticField[0]) self.magneticFieldYDoubleSpinBox.setValue(self.magneticField[1]) self.magneticFieldZDoubleSpinBox.setValue(self.magneticField[2]) # Set the labels, ranges, etc. self.energiesTabWidget.setTabText(0, self.energies[0][0]) self.e1MinDoubleSpinBox.setValue(self.energies[0][1]) self.e1MaxDoubleSpinBox.setValue(self.energies[0][2]) self.e1NPointsDoubleSpinBox.setValue(self.energies[0][3]) self.e1LorentzianBroadeningDoubleSpinBox.setValue(self.energies[0][4]) if 'RIXS' in self.experiment: tab = self.energiesTabWidget.findChild(QWidget, 'e2Tab') self.energiesTabWidget.addTab(tab, tab.objectName()) self.energiesTabWidget.setTabText(1, self.energies[1][0]) self.e2MinDoubleSpinBox.setValue(self.energies[1][1]) self.e2MaxDoubleSpinBox.setValue(self.energies[1][2]) self.e2NPointsDoubleSpinBox.setValue(self.energies[1][3]) self.e2LorentzianBroadeningDoubleSpinBox.setValue( self.energies[1][4]) self.e1GaussianBroadeningDoubleSpinBox.setEnabled(False) self.e2GaussianBroadeningDoubleSpinBox.setEnabled(False) else: self.energiesTabWidget.removeTab(1) self.e1GaussianBroadeningDoubleSpinBox.setEnabled(True) self.e2GaussianBroadeningDoubleSpinBox.setEnabled(True) self.nPsisDoubleSpinBox.setValue(self.nPsis) # Create the Hamiltonian model. self.hamiltonianModel = TreeModel(('Parameter', 'Value', 'Scaling'), self.hamiltonianParameters) self.hamiltonianModel.setNodesCheckState( self.hamiltonianTermsCheckState) # Assign the Hamiltonian model to the Hamiltonian terms view. self.hamiltonianTermsView.setModel(self.hamiltonianModel) self.hamiltonianTermsView.selectionModel().setCurrentIndex( self.hamiltonianModel.index(0, 0), QItemSelectionModel.Select) # Assign the Hamiltonian model to the Hamiltonian parameters view, and # set some properties. self.hamiltonianParametersView.setModel(self.hamiltonianModel) self.hamiltonianParametersView.expandAll() self.hamiltonianParametersView.resizeAllColumnsToContents() self.hamiltonianParametersView.setColumnWidth(0, 160) self.hamiltonianParametersView.setAlternatingRowColors(True) index = self.hamiltonianTermsView.currentIndex() self.hamiltonianParametersView.setRootIndex(index) self.hamiltonianTermsView.selectionModel().selectionChanged.connect( self.selectedHamiltonianTermChanged) # Set the sizes of the two views. self.hamiltonianSplitter.setSizes((120, 300)) # Create the results model and assign it to the view. if not hasattr(self, 'resultsModel'): self.resultsModel = ListModel() self.resultsView.setSelectionMode( QAbstractItemView.ExtendedSelection) self.resultsView.setModel(self.resultsModel) self.resultsView.selectionModel().selectionChanged.connect( self.selectedCalculationsChanged) # Add a context menu self.resultsView.setContextMenuPolicy(Qt.CustomContextMenu) self.resultsView.customContextMenuRequested[QPoint].connect( self.createContextMenu) self.resultsView.setAlternatingRowColors(True) def activateUi(self): self.elementComboBox.currentTextChanged.connect(self.updateParameters) self.chargeComboBox.currentTextChanged.connect(self.updateParameters) self.symmetryComboBox.currentTextChanged.connect(self.updateParameters) self.experimentComboBox.currentTextChanged.connect( self.updateParameters) self.edgeComboBox.currentTextChanged.connect(self.updateParameters) self.saveInputAsPushButton.clicked.connect(self.saveInputAs) self.calculationPushButton.clicked.connect(self.runCalculation) self.e1GaussianBroadeningDoubleSpinBox.valueChanged.connect( self.replot) self.e1LorentzianBroadeningDoubleSpinBox.valueChanged.connect( self.replot) def getParameters(self): self.element = self.elementComboBox.currentText() self.charge = self.chargeComboBox.currentText() self.symmetry = self.symmetryComboBox.currentText() self.experiment = self.experimentComboBox.currentText() self.edge = self.edgeComboBox.currentText() self.temperature = self.temperatureDoubleSpinBox.value() self.magneticField = ( self.magneticFieldXDoubleSpinBox.value(), self.magneticFieldYDoubleSpinBox.value(), self.magneticFieldZDoubleSpinBox.value(), ) self.nPsis = int(self.nPsisDoubleSpinBox.value()) if 'RIXS' in self.experiment: self.energies = ((self.energiesTabWidget.tabText(0), self.e1MinDoubleSpinBox.value(), self.e1MaxDoubleSpinBox.value(), int(self.e1NPointsDoubleSpinBox.value()), self.e1LorentzianBroadeningDoubleSpinBox.value(), self.e1GaussianBroadeningDoubleSpinBox.value()), (self.energiesTabWidget.tabText(1), self.e2MinDoubleSpinBox.value(), self.e2MaxDoubleSpinBox.value(), int(self.e2NPointsDoubleSpinBox.value()), self.e2LorentzianBroadeningDoubleSpinBox.value(), self.e2GaussianBroadeningDoubleSpinBox.value())) else: self.energies = ( (self.energiesTabWidget.tabText(0), self.e1MinDoubleSpinBox.value(), self.e1MaxDoubleSpinBox.value(), int(self.e1NPointsDoubleSpinBox.value()), self.e1LorentzianBroadeningDoubleSpinBox.value(), self.e1GaussianBroadeningDoubleSpinBox.value()), ) self.hamiltonianParameters = self.hamiltonianModel.getModelData() self.hamiltonianTermsCheckState = ( self.hamiltonianModel.getNodesCheckState()) def saveParameters(self): for key in self._defaults: try: self.calculation[key] = copy.deepcopy(self.__dict__[key]) except KeyError: self.calculation[key] = None def loadParameters(self, dictionary): for key in self._defaults: try: self.__dict__[key] = copy.deepcopy(dictionary[key]) except KeyError: self.__dict__[key] = None def createContextMenu(self, position): icon = QIcon(resourceFileName(os.path.join('gui', 'icons', 'save.svg'))) self.saveSelectedCalculationsAsAction = QAction( icon, 'Save Selected Calculations As...', self, triggered=self.saveSelectedCalculationsAs) icon = QIcon( resourceFileName(os.path.join('gui', 'icons', 'trash.svg'))) self.removeCalculationsAction = QAction( icon, 'Remove Selected Calculations', self, triggered=self.removeSelectedCalculations) self.removeAllCalculationsAction = QAction( icon, 'Remove All Calculations', self, triggered=self.removeAllCalculations) icon = QIcon( resourceFileName(os.path.join('gui', 'icons', 'folder-open.svg'))) self.loadCalculationsAction = QAction(icon, 'Load Calculations', self, triggered=self.loadCalculations) selection = self.resultsView.selectionModel().selection() selectedItemsRegion = self.resultsView.visualRegionForSelection( selection) cursorPosition = self.resultsView.mapFromGlobal(QCursor.pos()) if selectedItemsRegion.contains(cursorPosition): contextMenu = QMenu('Items Context Menu', self) contextMenu.addAction(self.saveSelectedCalculationsAsAction) contextMenu.addAction(self.removeCalculationsAction) contextMenu.exec_(self.resultsView.mapToGlobal(position)) else: contextMenu = QMenu('View Context Menu', self) contextMenu.addAction(self.loadCalculationsAction) contextMenu.addAction(self.removeAllCalculationsAction) contextMenu.exec_(self.resultsView.mapToGlobal(position)) def saveSelectedCalculationsAs(self): path, _ = QFileDialog.getSaveFileName(self, 'Save Calculations', 'untitled', 'Pickle File (*.pkl)') if path: os.chdir(os.path.dirname(path)) calculations = list(self.selectedCalculations()) calculations.reverse() with open(path, 'wb') as p: pickle.dump(calculations, p) def removeSelectedCalculations(self): indexes = self.resultsView.selectedIndexes() self.resultsModel.removeItems(indexes) def removeAllCalculations(self): self.resultsModel.reset() def loadCalculations(self): path, _ = QFileDialog.getOpenFileName(self, 'Load Calculations', '', 'Pickle File (*.pkl)') if path: with open(path, 'rb') as p: self.resultsModel.appendItems(pickle.load(p)) self.resultsView.selectionModel().setCurrentIndex( self.resultsModel.index(0, 0), QItemSelectionModel.Select) def saveInput(self): # Load the template file specific to the requested calculation. path = resourceFileName( os.path.join('modules', 'quanty', 'templates', '{}'.format(self.templateName))) try: with open(path) as p: template = p.read() except IOError as e: self.parent().statusBar().showMessage( 'Could not find template file: {}.'.format(self.templateName)) raise e self.getParameters() replacements = collections.OrderedDict() for shell in self.shells: replacements['$NElectrons_{}'.format(shell[0])] = shell[1] replacements['$T'] = self.temperature # If all components of the magnetic field are zero, # add a small contribution in the y-direction to make the simulation # converge faster. if all(value == 0.0 for value in self.magneticField): self.magneticField = (0.0, 0.00001, 0.0) replacements['$Bx'] = self.magneticField[0] replacements['$By'] = self.magneticField[1] replacements['$Bz'] = self.magneticField[2] replacements['$Emin1'] = self.energies[0][1] replacements['$Emax1'] = self.energies[0][2] replacements['$NE1'] = self.energies[0][3] # Broadening is done in the interface. value = self.e1LorentzianBroadeningDoubleSpinBox.minimum() replacements['$Gamma1'] = value if 'RIXS' in self.experiment: replacements['$Emin2'] = self.energies[1][1] replacements['$Emax2'] = self.energies[1][2] replacements['$NE2'] = self.energies[1][3] replacements['$Gamma1'] = self.energies[0][4] replacements['$Gamma2'] = self.energies[1][4] replacements['$NPsis'] = self.nPsis for term in self.hamiltonianParameters: if 'Coulomb' in term: name = 'H_coulomb' elif 'Spin-Orbit Coupling' in term: name = 'H_soc' elif 'Crystal Field' in term: name = 'H_cf' elif '3d-Ligands Hybridization' in term: name = 'H_3d_Ld_hybridization' elif '3d-4p Hybridization' in term: name = 'H_3d_4p_hybridization' configurations = self.hamiltonianParameters[term] for configuration, parameters in configurations.items(): if 'Initial' in configuration: suffix = 'i' elif 'Intermediate' in configuration: suffix = 'm' elif 'Final' in configuration: suffix = 'f' for parameter, (value, scaling) in parameters.items(): key = '${}_{}_value'.format(parameter, suffix) replacements[key] = '{}'.format(value) key = '${}_{}_scaling'.format(parameter, suffix) replacements[key] = '{}'.format(scaling) checkState = self.hamiltonianTermsCheckState[term] if checkState > 0: checkState = 1 replacements['${}'.format(name)] = checkState replacements['$baseName'] = self.baseName for replacement in replacements: template = template.replace(replacement, str(replacements[replacement])) with open(self.baseName + '.lua', 'w') as f: f.write(template) def saveInputAs(self): path, _ = QFileDialog.getSaveFileName( self, 'Save Quanty Input', '{}'.format(self.baseName + '.lua'), 'Quanty Input File (*.lua)') if path: self.baseName, _ = os.path.splitext(os.path.basename(path)) self.updateMainWindowTitle() os.chdir(os.path.dirname(path)) try: self.saveInput() except IOError: return def runCalculation(self): if 'win32' in sys.platform: self.command = 'Quanty.exe' else: self.command = 'Quanty' with open(os.devnull, 'w') as f: try: subprocess.call(self.command, stdout=f, stderr=f) except: self.parent().statusBar().showMessage( 'Could not find Quanty. Please install ' 'it and set the PATH environment variable.') return # Write the input file to disk. try: self.saveInput() except FileNotFoundError: return # You are about to run; I will give you a label, a unique identifier, # and a starting time. self.label = '{} | {} | {} | {} | {}'.format(self.element, self.charge, self.symmetry, self.experiment, self.edge) self.uuid = uuid.uuid4().hex self.startingTime = datetime.datetime.now() self.calculation = collections.OrderedDict() self.saveParameters() # Run Quanty using QProcess. self.process = QProcess() self.process.start(self.command, (self.baseName + '.lua', )) self.parent().statusBar().showMessage('Running {} {} in {}.'.format( self.command, self.baseName + '.lua', os.getcwd())) self.process.readyReadStandardOutput.connect(self.handleOutputLogging) self.process.started.connect(self.updateCalculationPushButton) self.process.finished.connect(self.processCalculation) def updateCalculationPushButton(self): icon = QIcon(resourceFileName(os.path.join('gui', 'icons', 'stop.svg'))) self.calculationPushButton.setIcon(icon) self.calculationPushButton.setText('Stop') self.calculationPushButton.setToolTip('Stop Quanty') self.calculationPushButton.disconnect() self.calculationPushButton.clicked.connect(self.stopCalculation) def resetCalculationPushButton(self): icon = QIcon(resourceFileName(os.path.join('gui', 'icons', 'play.svg'))) self.calculationPushButton.setIcon(icon) self.calculationPushButton.setText('Run') self.calculationPushButton.setToolTip('Run Quanty') self.calculationPushButton.disconnect() self.calculationPushButton.clicked.connect(self.runCalculation) def stopCalculation(self): self.process.kill() def processCalculation(self): # When did I finish? self.endingTime = datetime.datetime.now() # Reset the calculation button. self.resetCalculationPushButton() # Evaluate the exit code and status of the process. exitStatus = self.process.exitStatus() exitCode = self.process.exitCode() timeout = 10000 statusBar = self.parent().statusBar() if exitStatus == 0 and exitCode == 0: message = ('Quanty has finished successfully in ') delta = int((self.endingTime - self.startingTime).total_seconds()) hours, reminder = divmod(delta, 60) minutes, seconds = divmod(reminder, 60) if hours > 0: message += '{} hours {} minutes and {} seconds.'.format( hours, minutes, seconds) elif minutes > 0: message += '{} minutes and {} seconds.'.format(minutes, hours) else: message += '{} seconds.'.format(seconds) statusBar.showMessage(message, timeout) elif exitStatus == 0 and exitCode == 1: self.handleErrorLogging() statusBar.showMessage( ('Quanty has finished unsuccessfully. ' 'Check the logging window for more details.'), timeout) self.parent().splitter.setSizes((400, 200)) return # exitCode is platform dependend; exitStatus is always 1. elif exitStatus == 1: message = 'Quanty was stopped.' statusBar.showMessage(message, timeout) return # Copy back the details of the calculation, and overwrite all UI # changes done by the user during the calculation. self.loadParameters(self.calculation) # Initialize the spectra container. self.spectra = dict() e1Min = self.energies[0][1] e1Max = self.energies[0][2] e1NPoints = self.energies[0][3] self.spectra['e1'] = np.linspace(e1Min, e1Max, e1NPoints + 1) if 'RIXS' in self.experiment: e2Min = self.energies[1][1] e2Max = self.energies[1][2] e2NPoints = self.energies[1][3] self.spectra['e2'] = np.linspace(e2Min, e2Max, e2NPoints + 1) # Find all spectra in the current folder. pattern = '{}*.spec'.format(self.baseName) for spectrumName in glob.glob(pattern): try: spectrum = np.loadtxt(spectrumName, skiprows=5) except FileNotFoundError: return if '_iso.spec' in spectrumName: key = 'Isotropic' elif '_cd.spec' in spectrumName: key = 'Circular Dichroism' self.spectra[key] = -spectrum[:, 2::2] # Remove the spectrum file os.remove(spectrumName) self.updateSpectraComboBox() # Store the calculation details; have to encapsulate it into a list. self.saveParameters() self.resultsModel.appendItems([self.calculation]) # Update the selected item in the results view. self.resultsView.selectionModel().clearSelection() index = self.resultsModel.index(self.resultsModel.rowCount() - 1) self.resultsView.selectionModel().select(index, QItemSelectionModel.Select) # TODO: Move this action in a better place. self.parent().plotWidget.spectraComboBox.currentTextChanged.connect( self.plot) self.plot() def updateSpectraComboBox(self): self.parent().plotWidget.spectraComboBox.clear() keys = ('Isotropic', 'Circular Dichroism') for key in keys: if key in self.spectra: self.parent().plotWidget.spectraComboBox.addItem(key) self.parent().plotWidget.spectraComboBox.setCurrentIndex(0) def plot(self): if not self.spectra: return plotWidget = self.parent().plotWidget if 'RIXS' in self.experiment: plotWidget.setGraphXLabel('Incident Energy (eV)') plotWidget.setGraphYLabel('Energy Transfer (eV)') colormap = { 'name': 'viridis', 'normalization': 'linear', 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0 } plotWidget.setDefaultColormap(colormap) legend = self.label + self.uuid x = self.spectra['e1'] xMin = x.min() xMax = x.max() xNPoints = x.size xScale = (xMax - xMin) / xNPoints y = self.spectra['e2'] yMin = y.min() yMax = y.max() yNPoints = y.size yScale = (yMax - yMin) / yNPoints currentPlot = plotWidget.spectraComboBox.currentText() try: z = self.spectra[currentPlot] except KeyError: return plotWidget.addImage(z, origin=(xMin, yMin), scale=(xScale, yScale)) else: plotWidget.setGraphXLabel('Absorption Energy (eV)') plotWidget.setGraphYLabel('Absorption Cross Section (a.u.)') legend = self.label + self.uuid x = self.spectra['e1'] currentPlot = plotWidget.spectraComboBox.currentText() try: y = self.spectra[currentPlot] except KeyError: return y = y[:, 0] fwhm = self.e1GaussianBroadeningDoubleSpinBox.value() if fwhm: y = self.broaden(x, y, type='gaussian', fwhm=fwhm) * y.max() fwhm = (self.e1LorentzianBroadeningDoubleSpinBox.value() - self.e1LorentzianBroadeningDoubleSpinBox.minimum()) if fwhm: y = self.broaden(x, y, type='lorentzian', fwhm=fwhm) * y.max() plotWidget.addCurve(x, y, legend) def replot(self): # Whenever the broading changes, the data related to that plot # has to change. # index = self.resultsView.selectedIndexes()[0] # self.getParameters() # self.saveParameters() # index = self.resultsModel.replaceItem(index, self.calculation) # self.resultsView.selectionModel().select( # index, QItemSelectionModel.Select) self.plot() @staticmethod def broaden(x, y, type='gaussian', fwhm=None): yb = np.zeros_like(y) if type == 'gaussian': sigma = fwhm / 2.0 * np.sqrt(2.0 * np.log(2.0)) for xi, yi in zip(x, y): yb += yi / (sigma * np.sqrt(2.0 * np.pi)) * np.exp( -1.0 / 2.0 * ((x - xi) / sigma)**2) elif type == 'lorentzian': gamma = fwhm for xi, yi in zip(x, y): yb += yi / np.pi * (0.5 * gamma) / ((x - xi)**2 + (0.5 * gamma)**2) yb = yb / yb.max() return yb def selectedHamiltonianTermChanged(self): index = self.hamiltonianTermsView.currentIndex() self.hamiltonianParametersView.setRootIndex(index) def selectedCalculations(self): indexes = self.resultsView.selectedIndexes() for index in indexes: yield self.resultsModel.getIndexData(index) def selectedCalculationsChanged(self): self.parent().plotWidget.clear() for index in self.selectedCalculations(): self.loadParameters(index) self.updateSpectraComboBox() self.setUi() self.updateMainWindowTitle() def handleOutputLogging(self): self.process.setReadChannel(QProcess.StandardOutput) data = self.process.readAllStandardOutput().data() self.parent().loggerWidget.appendPlainText(data.decode('utf-8')) def handleErrorLogging(self): self.process.setReadChannel(QProcess.StandardError) data = self.process.readAllStandardError().data() self.parent().loggerWidget.appendPlainText(data.decode('utf-8')) def updateMainWindowTitle(self): title = 'Crispy - {}'.format(self.baseName + '.lua') self.parent().setWindowTitle(title)
class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. _started: Whether the underlying process is started. _proc: The underlying QProcess. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__(self, what, *, verbose=False, additional_env=None, parent=None): super().__init__(parent) self._what = what self.verbose = verbose self._started = False self.cmd = None self.args = None self._proc = QProcess(self) self._proc.error.connect(self.on_error) self._proc.error.connect(self.error) self._proc.finished.connect(self.on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self.on_started) self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) @pyqtSlot(QProcess.ProcessError) def on_error(self, error): """Show a message if there was an error while spawning.""" msg = ERROR_STRINGS[error] message.error("Error while spawning {}: {}".format(self._what, msg)) @pyqtSlot(int, QProcess.ExitStatus) def on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) if status == QProcess.CrashExit: message.error("{} crashed!".format(self._what.capitalize())) elif status == QProcess.NormalExit and code == 0: if self.verbose: message.info("{} exited successfully.".format( self._what.capitalize())) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. message.error("{} exited with status {}.".format( self._what.capitalize(), code)) stderr = bytes(self._proc.readAllStandardError()).decode('utf-8') stdout = bytes(self._proc.readAllStandardOutput()).decode('utf-8') if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) @pyqtSlot() def on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self._started self._started = True def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) log.procs.debug("Executing: {}".format(fake_cmdline)) if self.verbose: message.info('Executing: ' + fake_cmdline) def start(self, cmd, args, mode=None): """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) if mode is None: self._proc.start(cmd, args) else: self._proc.start(cmd, args, mode) def start_detached(self, cmd, args, cwd=None): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, cwd) if ok: log.procs.debug("Process started.") self._started = True else: message.error("Error while spawning {}: {}.".format( self._what, self._proc.error())) def exit_status(self): return self._proc.exitStatus()
class VideoService(QObject): def __init__(self, parent): super(VideoService, self).__init__(parent) self.parent = parent self.consoleOutput = '' if sys.platform == 'win32': self.backend = os.path.join(self.getAppPath(), 'bin', 'ffmpeg.exe') if not os.path.exists(self.backend): self.backend = find_executable('ffmpeg.exe') elif sys.platform == 'darwin': self.backend = os.path.join(self.getAppPath(), 'bin', 'ffmpeg') else: for exe in ('ffmpeg', 'avconv'): exe_path = find_executable(exe) if exe_path is not None: self.backend = exe_path break self.initProc() def initProc(self) -> None: self.proc = QProcess(self.parent) self.proc.setProcessChannelMode(QProcess.MergedChannels) env = QProcessEnvironment.systemEnvironment() self.proc.setProcessEnvironment(env) self.proc.setWorkingDirectory(self.getAppPath()) if hasattr(self.proc, 'errorOccurred'): self.proc.errorOccurred.connect(self.cmdError) def capture(self, source: str, frametime: str) -> QPixmap: img, capres = None, QPixmap() try: img = QTemporaryFile(os.path.join(QDir.tempPath(), 'XXXXXX.jpg')) if img.open(): imagecap = img.fileName() args = '-ss %s -i "%s" -vframes 1 -s 100x70 -y %s' % ( frametime, source, imagecap) if self.cmdExec(self.backend, args): capres = QPixmap(imagecap, 'JPG') finally: del img return capres def cut(self, source: str, output: str, frametime: str, duration: str) -> bool: args = '-i "%s" -ss %s -t %s -vcodec copy -acodec copy -y "%s"' \ % (source, frametime, duration, QDir.fromNativeSeparators(output)) return self.cmdExec(self.backend, args) def join(self, filelist: list, output: str) -> bool: args = '-f concat -safe 0 -i "%s" -c copy -y "%s"' % ( filelist, QDir.fromNativeSeparators(output)) return self.cmdExec(self.backend, args) def cmdExec(self, cmd: str, args: str = None) -> bool: if os.getenv('DEBUG', False): print('VideoService CMD: %s %s' % (cmd, args)) if self.proc.state() == QProcess.NotRunning: self.proc.start(cmd, shlex.split(args)) self.proc.waitForFinished(-1) if self.proc.exitStatus( ) == QProcess.NormalExit and self.proc.exitCode() == 0: return True return False @pyqtSlot(QProcess.ProcessError) def cmdError(self, error: QProcess.ProcessError) -> None: if error != QProcess.Crashed: QMessageBox.critical(self.parent.parent, '', '<h4>%s Error:</h4>' % self.backend + '<p>%s</p>' % self.proc.errorString(), buttons=QMessageBox.Close) qApp.quit() def getAppPath(self) -> str: if getattr(sys, 'frozen', False): return sys._MEIPASS return QFileInfo(__file__).absolutePath()
class VideoService(QObject): def __init__(self, parent): super(VideoService, self).__init__(parent) self.parent = parent self.consoleOutput = '' self.backend = 'ffmpeg' self.arch = 'x86' if platform.architecture()[0] == '32bit' else 'x64' if sys.platform == 'win32': self.backend = os.path.join(self.getAppPath(), 'bin', self.arch, 'ffmpeg.exe') self.initProc() def initProc(self) -> None: self.proc = QProcess(self.parent) self.proc.setProcessChannelMode(QProcess.MergedChannels) self.proc.setWorkingDirectory(self.getAppPath()) if hasattr(self.proc, 'errorOccurred'): self.proc.errorOccurred.connect(self.cmdError) def capture(self, source: str, frametime: str) -> QPixmap: img, capres = None, QPixmap() try: img = QTemporaryFile(os.path.join(QDir.tempPath(), 'XXXXXX.jpg')) if img.open(): imagecap = img.fileName() args = '-ss %s -i "%s" -vframes 1 -s 100x70 -y %s' % ( frametime, source, imagecap) if self.cmdExec(self.backend, args): capres = QPixmap(imagecap, 'JPG') finally: del img return capres def cut(self, source: str, output: str, frametime: str, duration: str) -> bool: args = '-i "%s" -ss %s -t %s -vcodec copy -acodec copy -y "%s"'\ % (source, frametime, duration, QDir.fromNativeSeparators(output)) return self.cmdExec(self.backend, args) def join(self, filelist: list, output: str) -> bool: args = '-f concat -safe 0 -i "%s" -c copy -y "%s"' % ( filelist, QDir.fromNativeSeparators(output)) return self.cmdExec(self.backend, args) def cmdExec(self, cmd: str, args: str = None) -> bool: if self.proc.state() == QProcess.NotRunning: self.proc.start(cmd, shlex.split(args)) self.proc.waitForFinished(-1) if self.proc.exitStatus( ) == QProcess.NormalExit and self.proc.exitCode() == 0: return True return False @pyqtSlot(QProcess.ProcessError) def cmdError(self, error: QProcess.ProcessError) -> None: if error != QProcess.Crashed: QMessageBox.critical(self.parent.parent, "Error calling an external process", self.proc.errorString(), buttons=QMessageBox.Cancel) def getAppPath(self) -> str: if getattr(sys, 'frozen', False): return sys._MEIPASS return QFileInfo(__file__).absolutePath()
class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. _started: Whether the underlying process is started. _proc: The underlying QProcess. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__(self, what, *, verbose=False, additional_env=None, parent=None): super().__init__(parent) self._what = what self.verbose = verbose self._started = False self.cmd = None self.args = None self._proc = QProcess(self) self._proc.errorOccurred.connect(self._on_error) self._proc.errorOccurred.connect(self.error) self._proc.finished.connect(self._on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self._on_started) self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) @pyqtSlot() def _on_error(self): """Show a message if there was an error while spawning.""" msg = self._proc.errorString() message.error("Error while spawning {}: {}".format(self._what, msg)) @pyqtSlot(int, QProcess.ExitStatus) def _on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) encoding = locale.getpreferredencoding(do_setlocale=False) stderr = bytes(self._proc.readAllStandardError()).decode( encoding, 'replace') stdout = bytes(self._proc.readAllStandardOutput()).decode( encoding, 'replace') if status == QProcess.CrashExit: exitinfo = "{} crashed!".format(self._what.capitalize()) message.error(exitinfo) elif status == QProcess.NormalExit and code == 0: exitinfo = "{} exited successfully.".format( self._what.capitalize()) if self.verbose: message.info(exitinfo) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. exitinfo = ("{} exited with status {}, see :messages for " "details.").format(self._what.capitalize(), code) message.error(exitinfo) if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) glimpsescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr) def _spawn_format(self, exitinfo, stdout, stderr): """Produce a formatted string for spawn output.""" stdout = (stdout or "(No output)").strip() stderr = (stderr or "(No output)").strip() spawn_string = ("{}\n" "\nProcess stdout:\n {}" "\nProcess stderr:\n {}").format(exitinfo, stdout, stderr) return spawn_string @pyqtSlot() def _on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self._started self._started = True def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) log.procs.debug("Executing: {}".format(fake_cmdline)) if self.verbose: message.info('Executing: ' + fake_cmdline) def start(self, cmd, args): """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start(cmd, args) self._proc.closeWriteChannel() def start_detached(self, cmd, args): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, None) if not ok: message.error("Error while spawning {}".format(self._what)) return False log.procs.debug("Process started.") self._started = True return True def exit_status(self): return self._proc.exitStatus()
class EricdocExecDialog(QDialog, Ui_EricdocExecDialog): """ Class implementing a dialog to show the output of the ericdoc process. This class starts a QProcess and displays a dialog that shows the output of the documentation command process. """ def __init__(self, cmdname, parent=None): """ Constructor @param cmdname name of the documentation generator (string) @param parent parent widget of this dialog (QWidget) """ super(EricdocExecDialog, self).__init__(parent) self.setModal(True) self.setupUi(self) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Cancel).setDefault(True) self.process = None self.cmdname = cmdname def start(self, args, fn): """ Public slot to start the ericdoc command. @param args commandline arguments for ericdoc program (list of strings) @param fn filename or dirname to be processed by ericdoc program (string) @return flag indicating the successful start of the process (boolean) """ self.errorGroup.hide() self.filename = fn if os.path.isdir(self.filename): dname = os.path.abspath(self.filename) fname = "." if os.path.exists(os.path.join(dname, "__init__.py")): fname = os.path.basename(dname) dname = os.path.dirname(dname) else: dname = os.path.dirname(self.filename) fname = os.path.basename(self.filename) self.contents.clear() self.errors.clear() program = args[0] del args[0] args.append(fname) self.process = QProcess() self.process.setWorkingDirectory(dname) self.process.readyReadStandardOutput.connect(self.__readStdout) self.process.readyReadStandardError.connect(self.__readStderr) self.process.finished.connect(self.__finish) self.setWindowTitle( self.tr('{0} - {1}').format(self.cmdname, self.filename)) self.process.start(program, args) procStarted = self.process.waitForStarted(5000) if not procStarted: E5MessageBox.critical( self, self.tr('Process Generation Error'), self.tr( 'The process {0} could not be started. ' 'Ensure, that it is in the search path.' ).format(program)) return procStarted def on_buttonBox_clicked(self, button): """ Private slot called by a button of the button box clicked. @param button button that was clicked (QAbstractButton) """ if button == self.buttonBox.button(QDialogButtonBox.Close): self.accept() elif button == self.buttonBox.button(QDialogButtonBox.Cancel): self.__finish() def __finish(self): """ Private slot called when the process finished. It is called when the process finished or the user pressed the button. """ if self.process is not None: if self.process.state() != QProcess.NotRunning: self.process.terminate() QTimer.singleShot(2000, self.process.kill) self.process.waitForFinished(3000) if self.process.exitStatus() == QProcess.CrashExit: self.contents.insertPlainText( self.tr('\n{0} crashed.\n').format(self.cmdname)) self.buttonBox.button(QDialogButtonBox.Close).setEnabled(True) self.buttonBox.button(QDialogButtonBox.Cancel).setEnabled(False) self.buttonBox.button(QDialogButtonBox.Close).setDefault(True) self.process = None self.contents.insertPlainText( self.tr('\n{0} finished.\n').format(self.cmdname)) self.contents.ensureCursorVisible() def __readStdout(self): """ Private slot to handle the readyReadStandardOutput signal. It reads the output of the process, formats it and inserts it into the contents pane. """ self.process.setReadChannel(QProcess.StandardOutput) while self.process.canReadLine(): s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') self.contents.insertPlainText(s) self.contents.ensureCursorVisible() def __readStderr(self): """ Private slot to handle the readyReadStandardError signal. It reads the error output of the process and inserts it into the error pane. """ self.process.setReadChannel(QProcess.StandardError) while self.process.canReadLine(): self.errorGroup.show() s = str(self.process.readLine(), Preferences.getSystem("IOEncoding"), 'replace') self.errors.insertPlainText(s) self.errors.ensureCursorVisible()
class GUIProcess(QObject): """An external process which shows notifications in the GUI. Args: cmd: The command which was started. args: A list of arguments which gets passed. verbose: Whether to show more messages. _output_messages: Show output as messages. _started: Whether the underlying process is started. _proc: The underlying QProcess. _what: What kind of thing is spawned (process/editor/userscript/...). Used in messages. Signals: error/finished/started signals proxied from QProcess. """ error = pyqtSignal(QProcess.ProcessError) finished = pyqtSignal(int, QProcess.ExitStatus) started = pyqtSignal() def __init__(self, what, *, verbose=False, additional_env=None, output_messages=False, parent=None): super().__init__(parent) self._what = what self.verbose = verbose self._output_messages = output_messages self._started = False self.cmd = None self.args = None self.final_stdout: str = "" self.final_stderr: str = "" self._proc = QProcess(self) self._proc.errorOccurred.connect(self._on_error) self._proc.errorOccurred.connect(self.error) self._proc.finished.connect(self._on_finished) self._proc.finished.connect(self.finished) self._proc.started.connect(self._on_started) self._proc.started.connect(self.started) if additional_env is not None: procenv = QProcessEnvironment.systemEnvironment() for k, v in additional_env.items(): procenv.insert(k, v) self._proc.setProcessEnvironment(procenv) @pyqtSlot(QProcess.ProcessError) def _on_error(self, error): """Show a message if there was an error while spawning.""" if error == QProcess.Crashed and not utils.is_windows: # Already handled via ExitStatus in _on_finished return what = f"{self._what} {self.cmd!r}" error_descriptions = { QProcess.FailedToStart: f"{what.capitalize()} failed to start", QProcess.Crashed: f"{what.capitalize()} crashed", QProcess.Timedout: f"{what.capitalize()} timed out", QProcess.WriteError: f"Write error for {what}", QProcess.WriteError: f"Read error for {what}", } error_string = self._proc.errorString() msg = ': '.join([error_descriptions[error], error_string]) # We can't get some kind of error code from Qt... # https://bugreports.qt.io/browse/QTBUG-44769 # However, it looks like those strings aren't actually translated? known_errors = ['No such file or directory', 'Permission denied'] if (': ' in error_string and # pragma: no branch error_string.split(': ', maxsplit=1)[1] in known_errors): msg += f'\n(Hint: Make sure {self.cmd!r} exists and is executable)' message.error(msg) @pyqtSlot(int, QProcess.ExitStatus) def _on_finished(self, code, status): """Show a message when the process finished.""" self._started = False log.procs.debug("Process finished with code {}, status {}.".format( code, status)) encoding = locale.getpreferredencoding(do_setlocale=False) stderr = self._proc.readAllStandardError().data().decode( encoding, 'replace') stdout = self._proc.readAllStandardOutput().data().decode( encoding, 'replace') if self._output_messages: if stdout: message.info(stdout.strip()) if stderr: message.error(stderr.strip()) if status == QProcess.CrashExit: exitinfo = "{} crashed.".format(self._what.capitalize()) message.error(exitinfo) elif status == QProcess.NormalExit and code == 0: exitinfo = "{} exited successfully.".format( self._what.capitalize()) if self.verbose: message.info(exitinfo) else: assert status == QProcess.NormalExit # We call this 'status' here as it makes more sense to the user - # it's actually 'code'. exitinfo = ("{} exited with status {}, see :messages for " "details.").format(self._what.capitalize(), code) message.error(exitinfo) if stdout: log.procs.error("Process stdout:\n" + stdout.strip()) if stderr: log.procs.error("Process stderr:\n" + stderr.strip()) qutescheme.spawn_output = self._spawn_format(exitinfo, stdout, stderr) self.final_stdout = stdout self.final_stderr = stderr def _spawn_format(self, exitinfo, stdout, stderr): """Produce a formatted string for spawn output.""" stdout = (stdout or "(No output)").strip() stderr = (stderr or "(No output)").strip() spawn_string = ("{}\n" "\nProcess stdout:\n {}" "\nProcess stderr:\n {}").format( exitinfo, stdout, stderr) return spawn_string @pyqtSlot() def _on_started(self): """Called when the process started successfully.""" log.procs.debug("Process started.") assert not self._started self._started = True def _pre_start(self, cmd, args): """Prepare starting of a QProcess.""" if self._started: raise ValueError("Trying to start a running QProcess!") self.cmd = cmd self.args = args fake_cmdline = ' '.join(shlex.quote(e) for e in [cmd] + list(args)) log.procs.debug("Executing: {}".format(fake_cmdline)) if self.verbose: message.info('Executing: ' + fake_cmdline) def start(self, cmd, args): """Convenience wrapper around QProcess::start.""" log.procs.debug("Starting process.") self._pre_start(cmd, args) self._proc.start(cmd, args) self._proc.closeWriteChannel() def start_detached(self, cmd, args): """Convenience wrapper around QProcess::startDetached.""" log.procs.debug("Starting detached.") self._pre_start(cmd, args) ok, _pid = self._proc.startDetached(cmd, args, None) # type: ignore[call-arg] if not ok: message.error("Error while spawning {}".format(self._what)) return False log.procs.debug("Process started.") self._started = True return True def exit_status(self): return self._proc.exitStatus()
class Process(QObject): """ Use the QProcess mechanism to run a subprocess asynchronously This will interact well with Qt Gui objects, eg by connecting the `output` signals to an `QTextEdit.append` method and the `started` and `finished` signals to a `QPushButton.setEnabled`. eg:: import sys from PyQt5.QtCore import * from PyQt5.QtWidgets import * class Example(QMainWindow): def __init__(self): super().__init__() textEdit = QTextEdit() self.setCentralWidget(textEdit) self.setGeometry(300, 300, 350, 250) self.setWindowTitle('Main window') self.show() self.process = Process() self.process.output.connect(textEdit.append) self.process.run(sys.executable, ["-u", "-m", "pip", "list"]) def main(): app = QApplication(sys.argv) ex = Example() sys.exit(app.exec_()) """ started = pyqtSignal() output = pyqtSignal(str) finished = pyqtSignal() Slots = namedtuple("Slots", ["started", "output", "finished"]) Slots.__new__.__defaults__ = (None, None, None) def __init__(self): super().__init__() # # Always run unbuffered and with UTF-8 IO encoding # self.environment = QProcessEnvironment.systemEnvironment() self.environment.insert("PYTHONUNBUFFERED", "1") self.environment.insert("PYTHONIOENCODING", "utf-8") def _set_up_run(self, **envvars): """Run the process with the command and args""" self.process = QProcess(self) environment = QProcessEnvironment(self.environment) for k, v in envvars.items(): environment.insert(k, v) self.process.setProcessEnvironment(environment) self.process.setProcessChannelMode(QProcess.MergedChannels) def run_blocking(self, command, args, wait_for_s=30.0, **envvars): self._set_up_run(**envvars) self.process.start(command, args) self.wait(wait_for_s=wait_for_s) return self.data() def run(self, command, args, **envvars): self._set_up_run(**envvars) self.process.readyRead.connect(self._readyRead) self.process.started.connect(self._started) self.process.finished.connect(self._finished) QTimer.singleShot( 100, functools.partial(self.process.start, command, args) ) def wait(self, wait_for_s=30.0): finished = self.process.waitForFinished(1000 * wait_for_s) # # If finished is False, it could be be because of an error # or because we've already finished before starting to wait! # if ( not finished and self.process.exitStatus() == self.process.CrashExit ): raise VirtualEnvironmentError("Some error occurred") def data(self): return self.process.readAll().data().decode("utf-8") def _started(self): self.started.emit() def _readyRead(self): self.output.emit(self.data().strip()) def _finished(self): self.finished.emit()