Esempio n. 1
0
    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")
Esempio n. 2
0
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
Esempio n. 3
0
    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
Esempio n. 5
0
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
Esempio n. 6
0
    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())
Esempio n. 7
0
    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())
Esempio n. 8
0
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
Esempio n. 10
0
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()
Esempio n. 11
0
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
Esempio n. 12
0
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
Esempio n. 13
0
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
Esempio n. 14
0
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()
Esempio n. 15
0
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))
Esempio n. 16
0
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)
Esempio n. 17
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
Esempio n. 18
0
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()
Esempio n. 19
0
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 &copy; %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)
Esempio n. 20
0
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')
Esempio n. 21
0
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()
Esempio n. 22
0
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)
Esempio n. 23
0
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()
Esempio n. 24
0
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()
Esempio n. 25
0
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()
Esempio n. 26
0
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()
Esempio n. 27
0
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()
Esempio n. 28
0
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()
Esempio n. 29
0
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()