Npyscreen und Subprocess

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
wooper
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 17:43

Hallo,
Bin gerade dabei einen einfachen Medienplayer zu bauen. Ziel soll es sein, Dateien einzulesen und abzuspielen. Die Ausgabe soll auf der Kommandozeile erfolgen, als ASCII-Ausgabe. Hierfür verwende ich den mplayer.
Um ein passendes CLI-"UI" zu bauen habe ich das Tool npyscreen verwendet.
Allerdings kann ich über das npyscreen-Widget ButtonPress keine shell commands aufrufen.
In meiner form rufe ich folgendes auf:

Code: Alles auswählen

def create(self):
	self.play = self.add(npyscreen.ButtonPress, name="Play Media")

def whenPressed(self):
        self.play()
        self.parent.parentApp.switchForm(None)
        
def play(self):
        cmd = "mplayer -really-quiet -vo caca ' DATEI '"
        args = shlex.split(cmd)
        subprocess.call(args)
Meine frage hierbei ist:
Eignet sich npyscreen (Vorgabenhinweis der Aufgabe) überhaupt für ein solches Vorgehen?
Vielen Dank für eure Antworten!
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Warum kannst Du keine shell-Kommandos aufrufen? Was passiert?
Einen literalen String mit shlex zu splitten ist umständlich, wenn man gleich die Liste angeben könnte.

Code: Alles auswählen

cmd = ["mplayer", "-really-quiet", "-vo", "caca", " DATEI "]
subprocess.call(cmd)
wobei DATEI wohl eine Variable sein soll?
Du überschreibst die Methode `play` mit dem Attribut `play` in `create`.
wooper
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 17:43

Hab jetzt nochmal ein anderes Bsp. erstellt:

Code: Alles auswählen

class FormObject(npyscreen.ActionForm, npyscreen.SplitForm)

def create(self):
	self.show_atx = 10
        self.show_aty = 2
        self.play = self.add(npyscreen.ButtonPress, name="Play Media")

def whenPressed(self):
        #self.playvid() - sollte die Methode aufrufen
        #self.parent.parentApp.switchForm(None) - Hiermit kann aus der form gewechselt werden
        hey = ["echo", """Hey"""] # dies dient als Bsp.
        subprocess.call(hey)
        
def playvid(self):
        cmd = ["mplayer", "-really-quiet", "-vo", "caca", " /Users/Dave/Projektbsrn/Erde.mp4 "]
        subprocess.call(cmd)

'DATEI' - war ein path zu einer Datei.

Wenn man im "UI" den Button "Play Media" drückt, passiert absolut gar nichts, die form reagiert nicht. Ist der Ansatz einfach falsch?
Das Programm ist übrigens etwas gekürzt (Wrapper, App-Aufruf sowie einige Widgets fehlen hier noch, kann ich aber gerne auch noch posten).
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

Hast Du denn die Dokumentation zu ButtonPress gelesen? Dort steht, wie man einen Knopf programmieren muss. Soweit ich sehe, fehlt das Argument when_pressed_function. Der Dateiname enthält sicherlich keine Leerzeichen.
Benutzeravatar
__blackjack__
User
Beiträge: 13110
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@wooper: Das Problem setzt viel früher an und hat überhaupt gar nichts mit `subprocess` zu tun, denn woher hast Du denn das da die `whenPressed()`-Methode auf dem `Form`-Objekt aufgerufen werden sollte? Da besteht doch überhaupt gar keine Verbindung zu dem `ButtonPress`-Objekt das durch das `Form.add()` erstellt wird. Du musst entweder eine Rückruffunktion beim `add()` mitgeben oder eine Klasse von `ButtonPress` ableiten und *dort* die `whenPressed()`-Methode überschreiben.

Die Bibliothek ist ziemlich blöd. Von der Rückruffunktion, die erst sehr spät dazu gekommen ist, wird in der Dokumentation abgeraten und es wird geraten lieber eine Klasse für einen Button abzuleiten. Die Entwickler stehen anscheinend sehr auf Klassen ableiten. Und javaesque Namensgebung. Und bei den Beispielen auf sauschlechte Namen. Mal als Beispiel das erste Beispiel einer kleinen Anwendung in der Einleitung in der Dokumentation:

Code: Alles auswählen

#!/usr/bin/env python
# encoding: utf-8

import npyscreen
class TestApp(npyscreen.NPSApp):
    def main(self):
        # These lines create the form and populate it with widgets.
        # A fairly complex screen in only 8 or so lines of code - a line for each control.
        F  = npyscreen.Form(name = "Welcome to Npyscreen",)
        t  = F.add(npyscreen.TitleText, name = "Text:",)
        fn = F.add(npyscreen.TitleFilename, name = "Filename:")
        fn2 = F.add(npyscreen.TitleFilenameCombo, name="Filename2:")
        dt = F.add(npyscreen.TitleDateCombo, name = "Date:")
        s  = F.add(npyscreen.TitleSlider, out_of=12, name = "Slider")
        ml = F.add(npyscreen.MultiLineEdit,
               value = """try typing here!\nMutiline text, press ^R to reformat.\n""",
               max_height=5, rely=9)
        ms = F.add(npyscreen.TitleSelectOne, max_height=4, value = [1,], name="Pick One",
                values = ["Option1","Option2","Option3"], scroll_exit=True)
        ms2= F.add(npyscreen.TitleMultiSelect, max_height =-2, value = [1,], name="Pick Several",
                values = ["Option1","Option2","Option3"], scroll_exit=True)

        # This lets the user interact with the Form.
        F.edit()

        print(ms.get_selected_objects())

if __name__ == "__main__":
    App = TestApp()
    App.run()
Das ist kein Python. Nicht von jemanden der wirklich in Python programmiert. Das ist zum schreiend weglaufen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
wooper
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 17:43

Der Tipp mir der whenPressed-Methode und deren richtigen Anwendung war tatsächlich äußerst hilfreich, danke dafür.
Der Medienplayer funktioniert nun einwandfrei, es lassen sich Videos/Musik Dateien einfach auswählen und abspielen, eine Playlist Funktion habe ich auch noch hinzugefügt.
Lediglich das Modul "subprocess" wurde durch das Modul "os" getauscht, da subprocess einen verarbeiteten String im Kontext des Codes nicht richtig verarbeiten konnte.
Danke!
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@wooper: os.system ist veraltet. Du hast also einen Fehler durch einen schlimmen Fehler ersetzt. Zeig doch, was du versucht hast, dann können wir dir helfen es richtig zu machen.
wooper
User
Beiträge: 4
Registriert: Freitag 29. Mai 2020, 17:43

So habe ich ihn zum Laufen bekommen:

Code: Alles auswählen


import npyscreen
import os


class MyButtonPress(npyscreen.ButtonPress): # hier wird der "play-button" definiert
    def whenPressed(self):
        self.play_vid()

    def play_vid(self):

        cmd = []
        for i in range(len(MediaButtonPress.path)):
            if i > 0:
                cmd.append("&&")
            cmd_add = ["mplayer", "-really-quiet", "-vo", "caca", MediaButtonPress.path[i]]
            cmd.extend(cmd_add)
        command = ""
        for entry in cmd:
            command += entry + " "

        os.system(command) # hier bereitete subprocess Probleme


class MediaButtonPress(npyscreen.ButtonPress): # hier werden eine oder mehrere Dateien ausgewählt und deren path in einen array geschrieben
    path = []

    def whenPressed(self):
        media = npyscreen.selectFile(select_dir="")
        MediaButtonPress.path.append(os.path.realpath(media))


class FormObject(npyscreen.ActionFormMinimal, npyscreen.SplitForm):
    def create(self):
        self.show_atx = 10
        self.show_aty = 2
        self.select = self.add(MediaButtonPress, name="Select Media")
        self.nextrely += 1
        self.play = self.add(MyButtonPress, name="Play Media")
        self.nextrely += 1
        self.exit = self.add(npyscreen.TitleFixedText, name="Press OK to exit")

    def on_ok(self):
        npyscreen.notify_confirm("Good Bye!", editw=1)
        self.parentApp.setNextForm(None)


class App(npyscreen.NPSAppManaged):
    def onStart(self):
        self.addForm('MAIN', FormObject, name="Mediaplayer done simple", lines=20, columns=60, draw_line_at=17, )


if __name__ == "__main__":
    app = App().run()

Der Code tut was er soll, am Ende der "play_vid" Methode befindet sich der "os.system"-Befehl. Zuvor hatte ich diesen Schritt mit "subprocess.call" probiert, allerdings hat mein Programm dann immer nur das erste Video einer Playlist abgespielt. Mit "os.system" wird die komplette Liste fehlerfrei abgespielt.
Benutzeravatar
__blackjack__
User
Beiträge: 13110
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@wooper: Einwandfrei, naja. Wenn man eine Playlist abgespielt hat, dann bleibt die ja bestehen, das heisst man kann dann immer nur noch weitere Dateien hinzufügen, aber die alten werden auch immer wieder abgespielt.

Zudem ist die Playlist letztlich eine globale Variable. Das macht man nicht. Der Name `path` für eine Liste mehreren Pfaden ist auch falsch. Wenn der Benutzer Abspielen wählt bevor er Dateien zu der Liste (kein Array, wie im Kommentar fälschlicherweise behauptet) hinzugefügt hat, wird trotzdem versucht ``mplayer`` auszuführen, was keinen Sinn macht.

Den Rückgabewert von `App.run()` an den Namen `app` zu binden macht keinen Sinn.

Der `Object`-Zusatz in `FormObject` ist sinnlos. Klar ist das ein Objekt das man damit erstellt – in Python ist *alles* ein Objekt was man an einen Namen binden kann.

Bei `MyButtonPress` macht das `My` keinen Sinn. Am Namen sollte man erkennen können was das Objekt repräsentiert, nicht wem es ”gehört”. Nach der Logik müsste man ja vor jeden selbst definierten Namen ein `my` setzen.

Warum die Rückgabewerte von den `add()`-Aufrufen an das `Form`-Objekt gebunden werden ist mir jetzt nicht so wirklich ersichtlich.

Bei `selectFile()` beim `select_dir`-Argument eine leere Zeichenkette zu übergeben ist falsch. Da wird ein `bool` erwartet und eine leere Zeichenkette verwirrt bloss den Leser und hat zudem auch noch den Effekt vom Default-Wert `False`. An der Stelle würde es Sinn machen `must_exist` anzugeben, damit der Benutzer nur Dateien eingeben kann die auch tatsächlich existieren.

``for i in range(len(sequence)):`` nur um `i` dann als Index in das Sequenzobjekt zu verwenden, ist in Python ein „anti-pattern“. Das macht man nicht. Man kann direkt über die Elemente einer Sequenz iterieren, ohne den Umweg über einen Index. Falls man *zusätzlich* eine laufende Zahl benötigt, gibt es die `enumerate()`-Funktion.

Die Zahl wird hier aber nur verwendet um den ersten Schleifendurchlauf von allen anderen zu unterscheiden. Da sollte man den Code aber eher umschreiben, so dass das nicht mehr nötig ist. Die "&&" sollen zwischen alle ``mplayer``-Aufrufe, also würde man erst die ganzen Aufrufe formatieren und dann am Emde die "&&" per `str.join()`-Methode dazwischen setzen. Die ganze `play_vid()` würde dann nur noch so aussehen:

Code: Alles auswählen

    def play_vid(self)
        os.system(
            " && ".join(
                f"mplayer -really-quiet -vo caca {path}" for path in self.paths
            )
        )
Wie schon geschrieben wurde ist `os.system()` aber die falsche Funktion. Wenn man Glück hat, dann funktioniert es einfach nur nicht, beispielsweise wenn Dateinamen mit Leerzeichen oder Zeichen die eine besondere Bedeutung für die Shell haben vorkommen, wie Zeilenumbrüche, Klammern, Anführungszeichen (einzeln/doppelt), Dollarzeichen, Semikolons, &-Zeichen, …. Wenn man Pech hat, benennt jemand absichtlich eine Datei so, dass dem Benutzerkonto alle Dateien gelöscht werden.

Die ``&&`` sind etwas was eine Shell ausführt, die kann man natürlich nicht `subprocess.call()` mitgeben, weil das keine Shell zwischen Deinem Programm und ``mplayer`` setzt, sondern ``mplayer`` direkt startet und dem dann einfach die "&&" also normale Argumente mit gibt, womit ``mplayer`` aber nichts anfangen kann. Wenn Du mehrere ``mplayer``-Aufrufe nacheinander ausführen willst, dann musst Du das halt tun, in einer Schleife.

Nur sehr oberflächlich getestet:

Code: Alles auswählen

#!/usr/bin/env python3
import os
import subprocess

import npyscreen


class SelectMediaButton(npyscreen.ButtonPress):
    def __init__(self, *args, **kwargs):
        self.paths = kwargs.pop("paths")
        npyscreen.ButtonPress.__init__(self, *args, **kwargs)

    def whenPressed(self):
        self.paths.append(
            os.path.realpath(npyscreen.selectFile(must_exist=True))
        )


class PlayButton(npyscreen.ButtonPress):
    def __init__(self, *args, **kwargs):
        self.paths = kwargs.pop("paths")
        npyscreen.ButtonPress.__init__(self, *args, **kwargs)

    def whenPressed(self):
        try:
            for path in self.paths:
                subprocess.run(
                    ["mplayer", "-really-quiet", "-vo", "caca", path],
                    check=True,
                )
        except subprocess.CalledProcessError:
            #
            # Beim ersten ``mplayer``-Aufruf der einen Fehlercode liefert
            # abbrechhen, genau wie eine Verkettung der Aufrufe in einer Shell
            # mit ``&&`` das tun würde.
            #
            pass


class Form(npyscreen.ActionFormMinimal, npyscreen.SplitForm):
    def create(self):
        self.paths = list()
        self.show_atx = 10
        self.show_aty = 2
        self.add(SelectMediaButton, name="Select Media", paths=self.paths)
        self.nextrely += 1
        self.add(PlayButton, name="Play Media", paths=self.paths)
        self.nextrely += 1
        self.add(npyscreen.TitleFixedText, name="Press OK to exit")

    def on_ok(self):
        npyscreen.notify_confirm("Good Bye!", editw=1)
        self.parentApp.setNextForm(None)


class App(npyscreen.NPSAppManaged):
    def onStart(self):
        self.addForm(
            "MAIN",
            Form,
            name="Mediaplayer done simple",
            lines=20,
            columns=60,
            draw_line_at=17,
        )


if __name__ == "__main__":
    App().run()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten