Python-Script: Vorstellung und Bitte um Review

Sempervivum

Erfahrenes Mitglied
Dieses Skript hat zwei Hauptfunktionen:
1. Zuweisen eines Thumbnail bzw. Posterimage zu einem MP4-Video.
2. Erzeugen einer Sidecar-Datei, die den VLC-Player steuert.
Code:
import sys
import os
from shutil import copyfile
import json
import vlc
import time
import re
import tkinter as tk
from PIL import ImageTk, Image

try:
    # texts for start and stop preview button:
    txtStartPreview = "Start Preview"
    txtStopPreview = "Stop Preview"
    # height of preview for thumb:
    hPreview = 200
    print(str(sys.argv))
    # path without extension:
    path = os.path.splitext(sys.argv[1])[0]
    # extension:
    extension = os.path.splitext(sys.argv[1])[1]
    # path of video:
    videoPath = sys.argv[1]
    # path for playerinfo:
    infoPath = path + ".playerinfo"
    # path for thumbnail:
    thumbPath = path + "-thumb.jpg"
    # path for temp video:
    tempPath = path + "-temp" + extension
    print(tempPath)

    def parseTimeStr(str):
        mo = re.match("(\d{2}):(\d{2}):(\d{2})(\.\d{0,3})?", str)
        if mo:
            grps = mo.groups()
            ms = int(grps[0]) * 3600 + int(grps[1]) * 60 + int(grps[2])
            if grps[3] != None:
                ms += float(grps[3])
        else:
            ms = -1
        return ms

    # create thumbnail:
    def createThumb():
        # ffmpeg -ss 00:00:45 -i test2.mp4 -vframes 10 -q:v 2 thumb%2d.jpg
        # delete old thumb if exists:
        if os.path.exists(thumbPath):
            os.remove(thumbPath)
        # create thumb by use of ffmpeg:
        os.system(
            "ffmpeg -ss "
            + entryTime.get()
            + r' -i "' +
            tempPath + '" -vframes 1 -q:v 2 "' + thumbPath + '"'
        )
        # display preview of thumbnail:
        img = Image.open(thumbPath)
        width, height = img.size
        newWidth = int(width * hPreview / height)
        img = img.resize((newWidth, hPreview), Image.ANTIALIAS)
        img = ImageTk.PhotoImage(img)
        label.configure(image=img)
        label.image = img
        applyButton.configure(state=tk.NORMAL)

    # apply thumbnail to video:
    def applyThumb():
        # ffmpeg -i video.mp4 -i image.png -map 1 -map 0 -c copy -disposition:0 attached_pic out.mp4
        os.system(
            r'ffmpeg -i "' +
            tempPath + '" -i "' + thumbPath +
            '" -map 1 -map 0 -c copy -disposition:0 attached_pic "' +
            videoPath + '"'
        )

    # Preview in VLC player:
    def previewStart():
        global player
        if previewButton.cget('text') == txtStartPreview:
            previewButton.configure(text=txtStopPreview)
            previewIsPlaying = True
            try:
                vlcInstance
            except:
                vlcInstance = vlc.Instance(
                    "--input-repeat=-1", "--fullscreen")
                player = vlcInstance.media_player_new()
                media = vlcInstance.media_new(sys.argv[1])
                media.get_mrl()
                player.set_media(media)
            media.add_option(
                "start-time=" + str(parseTimeStr(entryTime.get())))
            player.play()
        else:
            player.stop()
            previewButton.configure(text=txtStartPreview)
            previewIsPlaying = False

    # create file containing params:
    def saveParams():
        params = {"volume": 25, "startTime": parseTimeStr(entryTime.get())}
        jsonParams = json.dumps(params)
        f = open(infoPath, "w")
        f.write(jsonParams)
        f.close()

    # create temp video file:
    copyfile(videoPath, tempPath)

    # create GUI:
    root = tk.Tk()

    # create entry for time:
    entryTime = tk.Entry(root)
    entryTime.insert(0, "00:00:30")
    entryTime.pack(padx=5, pady=5)

    # frame for creating thumbs:
    frameThumb = tk.Frame(root, bd=2, relief=tk.GROOVE)
    frameThumb.pack(fill=tk.X)

    # create label for preview:
    label = tk.Label(frameThumb)
    label.pack(padx=5, pady=5)

    # create button for creating thumbnail:
    createButton = tk.Button(
        frameThumb, text="Create Thumb", command=createThumb)
    createButton.pack(padx=5, pady=5)

    # create button for applying thumbnail:
    applyButton = tk.Button(frameThumb, text="Apply Thumb",
                            command=applyThumb, state=tk.DISABLED)
    applyButton.pack(padx=5, pady=5)

    # frame for creating player info:
    frameParams = tk.Frame(root, bd=2, relief=tk.GROOVE)
    frameParams.pack(fill=tk.X)

    # create button for preview start and stop:
    previewButton = tk.Button(frameParams, text=txtStartPreview,
                              command=previewStart)
    previewButton.pack(padx=5, pady=5)

    # create button for saving player info:
    paramsButton = tk.Button(
        frameParams, text="Save Params", command=saveParams)
    paramsButton.pack(padx=5, pady=5)

    root.mainloop()
except:
    print("Error at line", sys.exc_info()[2].tb_lineno, sys.exc_info()[0])
Wie man sieht ist es schon ein wenig lang geworden und der nächste Schritt wäre, es objektorientiert anzulegen entspr. den beiden Hauptfunktionen.
Und eine Validierung der Zeit vor dem Speichern wäre noch angebracht.
Beim Abspielen wird dann der VLC-Player aufgerufen und Lautstärke und Startzeit übergeben:
Code:
import glob
import sys
import os
import json
import subprocess

try:
    print("args:", str(sys.argv))
    infoPath = os.path.splitext(sys.argv[1])[0] + ".playerinfo"
    if os.path.isfile(infoPath):
        print(infoPath)
        f = open(infoPath, "r")
        info = json.loads(f.read())
        f.close()
        print(str(info))
    else:
        info = {'volume': 25, 'startTime': 0}
    vlcPath = "C:\\Program Files\\Multimedia\\VLC\\vlc.exe"
    print(str(info['volume']))
    subprocess.call([
        vlcPath, sys.argv[1],
        "--directx-volume=" + str(info['volume']),
        ":start-time=" + str(info['startTime'])
    ])
except:
    print("Error at line", sys.exc_info()[2].tb_lineno, sys.exc_info()[0])

input()
 
Was mir sofort auffällt:
Du solltest nicht deinen ganzen Code in einen try Block packen.
Wenn du mit Dateien arbeitest, solltest du with verwenden.
Code:
with open(file,'r') as inf:
'''Was mit der Datei passiert'''
anstelle von
Code:
inf = open(file,'r')
'''Was mit der Datei passiert'''
inf.close()
Es gibt ein Pythoninterface zu ffmpeg, pyffmpeg, mit dem du einige Sachen etwas vereinfachen könntest.

Benutze tuple unpacking um Funktionsaufrufe zu sparen
Code:
path,extension = os.path.splitext(sys.argv[1])
statt
Code:
# path without extension:
path = os.path.splitext(sys.argv[1])[0]
# extension:
extension = os.path.splitext(sys.argv[1])[1]
 
Zuletzt bearbeitet:
Danke für die Hinweise.
Du solltest nicht deinen ganzen Code in einen try Block packen.
Damit bin ich auch unzufrieden, aber es war die einzige Möglichkeit, die ich gefunden habe, um die Fehlermeldungen zu Gesicht zu bekommen. Ohne das try-catch bin ich immer aus dem Skript heraus geflogen ohne zu wissen, woran es fehlt. Alternative Vorschläge wären sehr willkommen.

Es gibt ein Pythoninterface zu ffmpeg, pyffmpeg, mit dem du einige Sachen etwas vereinfachen könntest.
Das ist ja Klasse, die Befehle für die Kommandozeile aufzubauen ist nicht immer ganz einfach. Hätte ich wissen müssen, als ich mit dem Skript begonnen habe.

Das mit dem "tupel unpacking" werde ich mir genauer ansehen, kann u. U. häufig ganz nützlich sein.
 
Tupel unpacking brauchst sehr oft. Ich schaffe es heute nicht mehr, aber vielleicht können wir morgen mal im Discord sprechen, dass finde ich einfacher als Zeile für Zeile zu kopieren und kommentieren.
 
finde ich einfacher als Zeile für Zeile zu kopieren und kommentieren.
Ich mache es häufig so, wenn ich eine Frage beantworte, dass ich den kompletten Code hinein kopiere und Kommentare einfüge, dann kann man es jederzeit nachlesen.

Zunächst mal eine gezielte Frage: Bei CSS kann man ja eine ganze Sammlung von Elementen stylen indem man mit einer Klasse arbeitet. Bei tkinter habe ich dafür die Funktion style gefunden aber anscheinend lässt sich diese nur auf ein Widget anwenden und nicht auf die grid- oder pack-Anweisung. Habe ich da etwas übersehen? Bei mir taucht nämlich dieses:
Code:
snapButtonStart.grid(row=1, column=0, padx=5, pady=5, sticky=tk.EW)
mehrfach auf und ich finde es ungünstig, es zu wiederholen.
Edit: Gerade ist mir eingefallen, dass ich ja einfach eine Funktion verwenden könnte, der ich das Element sowie row und column übergeben. Wäre das zu empfehlen?
 
Zuletzt bearbeitet:
Inzwischen war ich nicht untätig und habe das Skript weiter entwickelt:
Code:
import sys
import os
import errno
from shutil import copyfile
import json
import vlc
from vlc import MediaParseFlag, Meta
import time
import re
from functools import partial
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from PIL import ImageTk, Image

try:
    # get directory of script:
    scriptPath = os.path.dirname(os.path.abspath(__file__))
    # paths for player icons
    pathPlayImg = scriptPath + r'\images\buttonplay.png'
    pathPauseImg = scriptPath + r'\images\buttonpause.png'
    pathStopImg = scriptPath + r'\images\buttonstop.png'

    previewStat = 'stopped'
    # texts for start and stop preview button:
    txtStartPreview = "Start Preview"
    txtStopPreview = "Stop Preview"
    # height of preview for thumb:
    hPreview = 200
    (str(sys.argv))
    # path without extension:
    path = os.path.splitext(sys.argv[1])[0]
    # extension:
    extension = os.path.splitext(sys.argv[1])[1]
    # path of video:
    videoPath = sys.argv[1]
    # path for playerinfo:
    infoPath = path + ".playerinfo"
    # path for thumbnail:
    thumbPath = path + "-thumb.jpg"
    # path for temp video:
    tempPath = path + "-temp" + extension
    # duration of video:
    duration = -1

    def parseTimeStr(str):
        mo = re.match("(\d{2}):(\d{2}):(\d{2})(\.\d{1,3})?", str)
        if mo:
            grps = mo.groups()
            secs = int(grps[0]) * 3600 + int(grps[1]) * 60 + int(grps[2])
            if grps[3] != None:
                secs += float(grps[3])
            ms = int(secs * 1000)
        else:
            ms = -1
        return ms

    def formatTimeStr(ms):
        msecs = ms % 1000
        hSecs = int(msecs/100)
        secs = int(ms / 1000) % 60
        minutes = int(ms / 60000)  # % 60
        hours = int(ms / 3600000) % 3600000
        # formatted = "{0:0>2d}:{1:0>2d}:{2:0>2d}.{3:0>3d}".format(
        #     hours, minutes, secs, msecs
        # )
        formatted = "{0:0>2d}:{1:0>2d}:{2:0>2d}.{3:d}".format(
            hours, minutes, secs, hSecs
        )
        return formatted

    # create thumbnail:
    def createThumb():
        # ffmpeg -ss 00:00:45 -i test2.mp4 -vframes 10 -q:v 2 thumb%2d.jpg
        # delete old thumb if exists:
        if os.path.exists(thumbPath):
            os.remove(thumbPath)
        # create thumb by use of ffmpeg:
        os.system(
            "ffmpeg -ss "
            + entryTimeThumb.get()
            + r' -i "' +
            tempPath + '" -vframes 1 -q:v 2 "' + thumbPath + '"'
        )
        # display preview of thumbnail:
        img = Image.open(thumbPath)
        width, height = img.size
        newWidth = int(width * hPreview / height)
        img = img.resize((newWidth, hPreview), Image.ANTIALIAS)
        img = ImageTk.PhotoImage(img)
        label.configure(image=img)
        label.image = img
        applyButton.configure(state=tk.NORMAL)

    # apply thumbnail to video:
    def applyThumb():
        # ffmpeg -i video.mp4 -i image.png -map 1 -map 0 -c copy -disposition:0 attached_pic out.mp4
        answer = messagebox.askquestion(
            "Warnung", "Das originale Video wird überschrieben.\nMöchtest Du fortfahren?"
        )
        if answer == 'yes':
            error = False
            try:
                os.remove(videoPath)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    messagebox.showerror("Fehler",
                                         "Fehler beim Löschen des originalen Videos\n \
                        ist die Datei im Player geöffnet?"
                                         )
                    error = True
            if not error:
                os.system(
                    r'ffmpeg -i "' +
                    tempPath + '" -i "' + thumbPath +
                    '" -map 1 -map 0 -c copy -disposition:0 attached_pic "' +
                    videoPath + '"'
                )

    # move thumbnail one step (100 ms)
    def moveThumb(step):
        time = parseTimeStr(entryTimeThumb.get())
        time += step
        entryTimeThumb.delete(0, tk.END)
        entryTimeThumb.insert(0, formatTimeStr(time))
        createThumb()

    # snap time for thumb: transfer playing time from label to input:
    def snapTimeThumb():
        entryTimeThumb.delete(0, tk.END)
        entryTimeThumb.insert(0, formatTimeStr(player.get_time()) + '.0')

    # snap start time: transfer playing time from label to input:
    def snapTimeStart():
        entryTimeStart.delete(0, tk.END)
        entryTimeStart.insert(0, formatTimeStr(player.get_time()) + '.0')

    # snap end time: transfer playing time from label to input:
    def snapTimeEnd():
        entryTimeEnd.delete(0, tk.END)
        entryTimeEnd.insert(0, formatTimeStr(player.get_time()) + '.0')

    # event handler for change of playing time:
    def vlcTimeChanged(self, player):
        global labelTime
        print(str(media.get_parsed_status()))
        # if str(media.get_parsed_status()) == 'MediaParsedStatus.done':
        if str(media.get_parsed_status()) == 'MediaParsedStatus.done':
            txt = formatTimeStr(player.get_time()) + \
                '/' + formatTimeStr(media.get_duration())
        else:
            txt = formatTimeStr(player.get_time())
        labelTime.configure(text=txt)

    # create player for preview
    vlcInstance = vlc.Instance(
        "--input-repeat=-1", "--fullscreen")
    player = vlcInstance.media_player_new()
    media = vlcInstance.media_new(sys.argv[1])
    media.get_mrl()
    player.set_media(media)
    # add event listener for change of playing time:
    vlc_events = player.event_manager()
    vlc_events.event_attach(
        vlc.EventType.MediaPlayerTimeChanged, vlcTimeChanged, player
    )
    result = media.parse_with_options(MediaParseFlag.local, 0)

    def previewStart():

        global previewStat, startPauseButton
        if previewStat == 'stopped':
            startPauseButton.configure(image=pauseImg)
            previewStat = 'playing'
            media.add_option(
                "start-time=" + str(parseTimeStr(entryTimeStart.get()) / 1000)
            )
            player.play()
        elif previewStat == 'paused':
            startPauseButton.configure(image=pauseImg)
            previewStat = 'playing'
            player.play()

        elif previewStat == 'playing':
            startPauseButton.configure(image=playImg)
            previewStat = 'paused'
            player.pause()

    def previewStop():
        global previewStat
        player.stop()
        previewStat = 'stopped'
        startPauseButton.configure(image=playImg)

    # create file containing params:
    def saveParams():
        formatOk = True
        startTime = parseTimeStr(entryTimeStart.get())
        if startTime != -1:
            params = {"volume": entryVolume.get(), "startTime": startTime}
            if entryTimeEnd.get() != '':
                endTime = parseTimeStr(entryTimeEnd.get())
                if endTime != -1:
                    params['endTime'] = endTime
                else:
                    messagebox.showerror("Error", "Wrong format for end time")
                    formatOk = False
        else:
            messagebox.showerror("Error", "Wrong format for start time")
            formatOk = False
        if formatOk:
            jsonParams = json.dumps(params)
            with open(infoPath, "w") as f:
                f.write(jsonParams)

    # create temp video file:
    copyfile(videoPath, tempPath)

    # *** create GUI ***
    root = tk.Tk()
    root.title('My Video Utility')

    # *** preview ***

    # create images for preview:
    playImg = tk.PhotoImage(file=pathPlayImg)
    pauseImg = tk.PhotoImage(file=pathPauseImg)
    stopImg = tk.PhotoImage(file=pathStopImg)

    # frame for preview:
    framePreview = tk.Frame(root, bd=2, relief=tk.GROOVE)
    framePreview.pack(fill=tk.X)

    # configure grid for preview:
    framePreview.columnconfigure(0, weight=1)
    framePreview.columnconfigure(1, weight=1)

    # create label for playing time:
    labelTime = tk.Label(framePreview)
    labelTime.grid(row=0, column=0, columnspan=2, padx=5, pady=5)

    # create button for preview stop:
    stopButton = tk.Button(framePreview, bd=0, image=stopImg,
                           command=previewStop)
    stopButton.grid(row=1, column=0, padx=5, pady=5, sticky=tk.E)

    # create button for preview start, pause and resume:
    startPauseButton = tk.Button(framePreview, bd=0, image=playImg,
                                 command=previewStart)
    startPauseButton.grid(row=1, column=1, padx=5, pady=5, sticky=tk.W)

    # *** player params ***

    # frame for player params:
    frameParams = tk.Frame(root, bd=2, relief=tk.GROOVE)
    frameParams.pack(fill=tk.X)

    # configure grid for player params:
    frameParams.columnconfigure(0, weight=1)
    frameParams.columnconfigure(1, weight=0)

    # create label for volume:
    labelVolume = tk.Label(frameParams, text='Volume')
    labelVolume.grid(row=0, column=0, padx=5, pady=5)

    # create entry for volume:
    entryVolume = tk.Entry(frameParams, width=10)
    entryVolume.insert(0, 50)
    entryVolume.grid(row=0, column=1, padx=5, pady=5)

    # create button for snapping start time:
    snapButtonStart = tk.Button(frameParams, text="Snap Start Time",
                                command=snapTimeStart)
    snapButtonStart.grid(row=1, column=0, padx=5, pady=5, sticky=tk.EW)

    # create entry for time for start:
    entryTimeStart = tk.Entry(frameParams, width=10)
    entryTimeStart.insert(0, "00:00:30")
    entryTimeStart.grid(row=1, column=1, padx=5, pady=5)

    # create button for snapping end time:
    snapButtonEnd = tk.Button(frameParams, text="Snap End Time",
                              command=snapTimeEnd)
    snapButtonEnd.grid(row=2, column=0, padx=5, pady=5, sticky=tk.EW)

    # create entry for time for end:
    entryTimeEnd = tk.Entry(frameParams, width=10)
    entryTimeEnd.grid(row=2, column=1, padx=5, pady=5)

    # create button for saving player params:
    paramsButton = tk.Button(
        frameParams, text="Save Params", command=saveParams)
    paramsButton.grid(row=3, column=0, padx=5, pady=5, sticky=tk.EW)

    # *** Thumbnails ***

    # frame for creating thumbs:
    frameThumb = tk.Frame(root, bd=2, relief=tk.GROOVE)
    frameThumb.pack(fill=tk.X)

    frameThumb.columnconfigure(0, weight=1)
    frameThumb.columnconfigure(1, weight=0)

    # create label for thumbnail preview:
    label = tk.Label(frameThumb)
    label.grid(row=0, column=0, columnspan=2, padx=5, pady=5)

    # create frame for +/- buttons:
    framePlusMinus = tk.Frame(frameThumb)
    framePlusMinus.grid(row=1, column=0, columnspan=2, padx=5, pady=5)

    # create button for -100ms:
    snapButtonThumb = tk.Button(framePlusMinus, text="- 100 ms",
                                command=partial(moveThumb, -100))
    snapButtonThumb.grid(row=0, column=0, padx=5, pady=5, sticky=tk.E)

    # create button for +100ms:
    snapButtonThumb = tk.Button(framePlusMinus, text="+ 100 ms",
                                command=partial(moveThumb, +100))
    snapButtonThumb.grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)

    # create button for snapping time for thumbnail:
    snapButtonThumb = tk.Button(frameThumb, text="Snap Thumb Time",
                                command=snapTimeThumb)
    snapButtonThumb.grid(row=2, column=0, padx=5, pady=5, sticky=tk.EW)

    # create entry for time for thumbnail:
    entryTimeThumb = tk.Entry(frameThumb, width=10)
    entryTimeThumb.insert(0, "00:00:30")
    entryTimeThumb.grid(row=2, column=1, padx=5, pady=5)

    # create button for creating thumbnail:
    createButton = tk.Button(
        frameThumb, text="Create Thumb", command=createThumb)
    createButton.grid(row=3, column=0, padx=5, pady=5, sticky=tk.EW)

    # create button for applying thumbnail:
    applyButton = tk.Button(frameThumb, text="Apply Thumb",
                            command=applyThumb, state=tk.DISABLED)
    applyButton.grid(row=4, column=0, padx=5, pady=5, sticky=tk.EW)

    root.mainloop()
except:
    print("Error at line", sys.exc_info()[2].tb_lineno, sys.exc_info()[0])
input()
 
Was das styling betrifft: Ich arbeite mehr mit Qt. Da würde ich für sowas eine abgeleitete Klasse des Widgets erstellen und das Styling innerhalb der Klasse vornehmen. Ich würde dir auch empfehlen, deine UI Elemente von den Funktionen zu trennen. Mach mehrere Dateien und benutze from x import y.
 
Mach mehrere Dateien und benutze from x import y.
Daran dachte ich auch schon aber ich weiß bisher nicht, wie das funktioniert. In dieser Import-Anweisung ist ja nichts von Dateinamen zu sehen? Hiernach:
Python Modules: Learn to Create and Import Custom and Built-in Modules
ist der Modulname einfach der Dateiname ohne Erweiterung. Soweit glaube ich, es zu verstehen. Aber wie verhält es sich, wenn eine Funktion aus einem Modul aus einem anderen heraus aufgerufen wird? Z. B. sind an einige Buttons des UI ja Funktionen angehängt, die zum Preview-Player gehören.
 
Zurück