pytable/vmgr/main.py
2024-09-02 20:45:46 +02:00

446 lines
17 KiB
Python

import vlc
import sys
import os.path
import vlc
from PyQt5 import QtWidgets, QtCore, QtGui
import subprocess
from video import VideoModel
from superqt import QLabeledRangeSlider
from actions import UndoableAction, HistoryManager
from typing import List
import os
from datetime import datetime
history = HistoryManager()
class Player(QtWidgets.QMainWindow):
"""A simple Media Player using VLC and Qt
"""
def __init__(self, master=None):
QtWidgets.QMainWindow.__init__(self, master)
self.setWindowTitle("Media Player")
# creating a basic vlc instance
self.instance = vlc.Instance()
# creating an empty vlc media player
self.mediaplayer = self.instance.media_player_new()
self.createUI()
self.isPaused = False
def createUI(self):
"""Set up the user interface, signals & slots
"""
self.widget = QtWidgets.QWidget(self)
self.setCentralWidget(self.widget)
# In this widget, the video will be drawn
if sys.platform == "darwin": # for MacOS
self.videoframe = QtWidgets.QMacCocoaViewContainer(0)
else:
self.videoframe = QtWidgets.QFrame()
self.palette = self.videoframe.palette()
self.palette.setColor (QtGui.QPalette.Window,
QtGui.QColor(0,0,0))
self.videoframe.setPalette(self.palette)
self.videoframe.setAutoFillBackground(True)
self.positionslider = QtWidgets.QSlider(QtCore.Qt.Horizontal, self)
self.positionslider.setToolTip("Position")
self.positionslider.setMaximum(1000)
def mousePressEvent(event):
if event.button() == QtCore.Qt.LeftButton:
val = self.pixelPosToRangeValue(event.pos())
self.setValue(val)
self.positionslider.sliderMoved.connect(self.setPosition)
self.positionslider.sliderPressed.connect(lambda *args: print(args))
self.hbuttonbox = QtWidgets.QHBoxLayout()
self.playbutton = QtWidgets.QPushButton("Play")
self.hbuttonbox.addWidget(self.playbutton)
self.playbutton.clicked.connect(self.PlayPause)
self.hbuttonbox.addStretch(1)
self.setVolume(100)
self.videoplayerlayout = QtWidgets.QVBoxLayout()
self.videoplayerlayout.addWidget(self.videoframe)
self.videoplayerlayout.addWidget(self.positionslider)
self.videoplayerlayout.addLayout(self.hbuttonbox)
self.videoplayerlayoutWidget = QtWidgets.QWidget();
self.videoplayerlayoutWidget.setLayout(self.videoplayerlayout);
import dataclasses
def move_current_frame(d_frames):
before = self.mediaplayer.get_time()
mspf = int(1000 // (self.mediaplayer.get_fps() or 25))
d_time_ms = d_frames * mspf
target = self.mediaplayer.get_time() + d_time_ms
result = self.mediaplayer.set_time(target)
print(before, target, result, self.mediaplayer.get_time())
def pp():
self.PlayPause()
print(self.mediaplayer.get_time())
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Space"), self)
self.shortcut.activated.connect(pp)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Right"), self)
self.shortcut.activated.connect(lambda : move_current_frame(1))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Left"), self)
self.shortcut.activated.connect(lambda : move_current_frame(-1))
def export_frame():
print("XX")
t = self.mediaplayer.get_time()
print(t)
ms = t % 1000
t = t // 1000
s = t % 60
t = t // 60
m = t % 60
t = t // 60
h = t % 60
t = t // 60
t_str = f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"
print("Exporting: ", self.filename, t_str)
import os
from datetime import timedelta
def apply(filename, t_str):
base_dir = os.path.expanduser(os.path.join("~", "Pictures", "video_exports"))
print("W", filename, t_str)
if not os.path.exists(base_dir):
os.makedirs(base_dir)
print("W", filename, t_str)
begin = datetime.now()
base_name = os.path.splitext(os.path.basename(filename))[0]
print("W", filename, t_str)
print(base_name[:13])
frame_datetime = datetime.strptime(base_name[:13], "%Y%m%d_%H%M") + timedelta(hours=h, minutes=m, seconds=s, milliseconds=ms)
print(base_name[:13])
frame_datetime_str = frame_datetime.strftime("%Y:%m:%d_%H:%M:%S")
print(base_name[:13])
print("W", filename, t_str)
out = os.path.join(base_dir,
base_name +
f"{h:02d}{m:02d}{s:02d}{ms:03d}.png")
print(out)
subprocess.call([
"ffmpeg",
"-ss", t_str,
"-i", filename,
"-vframes", "1",
"-c:v", "png",
out
])
subprocess.call([
"exiftool",
f"-CreateDate={frame_datetime_str}",
f"-DateTimeOriginal={frame_datetime_str}",
f"-PNG:CreationTime={frame_datetime_str}",
"-overwrite_original",
out
])
print("Duration: ", datetime.now() - begin)
from video import thumbnailer
thumbnailer.pool.apply_async(apply, args=(self.filename, t_str))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+e"), self)
self.shortcut.activated.connect(lambda : export_frame())
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("f"), self)
self.shortcut.activated.connect(lambda : self.mediaplayer.toggle_fullscreen())
def slowmo():
if self.mediaplayer.get_rate() < 0.7:
print("rate", 1)
self.mediaplayer.set_rate(1)
else:
print("rate", .5)
self.mediaplayer.set_rate(0.5)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("s"), self)
self.shortcut.activated.connect(slowmo)
def change_size(diff):
self.icon_size += diff
print(self.icon_size < 64)
if self.icon_size < 64:
self.icon_size = 64
if self.icon_size > 256:
self.icon_size = 256
print(self.icon_size)
self.list_view.setIconSize(QtCore.QSize(self.icon_size, self.icon_size))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("+"), self)
self.shortcut.activated.connect(lambda : change_size(64))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("-"), self)
self.shortcut.activated.connect(lambda : change_size(-64))
from typing import Optional
def undo():
history.undo(self.model)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+z"), self)
self.shortcut.activated.connect(lambda : undo())
def redo():
history.redo(self.model)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+Shift+z"), self)
self.shortcut.activated.connect(lambda : redo())
def set_rating(value):
selected = self.list_view.selectedIndexes()
stars = None
actions = []
for i, index in enumerate(sorted(selected, key=lambda r: -r.row())):
video = self.model.video_files[index.row()]
from actions import SetVideoStars
stars = SetVideoStars((
video.fn,
video.metadata.rejected,
video.metadata.rating,
False if value >= 0 else not video.metadata.rejected,
value if value >= 0 else video.metadata.rating
), stars)
stars.run(self.model)
actions.append(stars)
history.apply(self.model, *actions)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("0"), self)
self.shortcut.activated.connect(lambda : set_rating(0))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("1"), self)
self.shortcut.activated.connect(lambda : set_rating(1))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("2"), self)
self.shortcut.activated.connect(lambda : set_rating(2))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("3"), self)
self.shortcut.activated.connect(lambda : set_rating(3))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("4"), self)
self.shortcut.activated.connect(lambda : set_rating(4))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("5"), self)
self.shortcut.activated.connect(lambda : set_rating(5))
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("r"), self)
self.shortcut.activated.connect(lambda : set_rating(-1))
def link():
directory = QtWidgets.QFileDialog.getExistingDirectory(
self, "Open File", os.path.expanduser('~'))
if not os.path.isdir(directory):
QtWidgets.QMessageBox.warning(
self,
"Not a directory",
"The selected item was not a directory. "
"You need to select an empty folder. ")
return
if os.listdir(directory):
QtWidgets.QMessageBox.warning(
self,
"Non-empty directory selected",
"The selected directory is not empty. "
"You need to select an empty folder. ")
return
print(directory)
failure = False
for index in self.list_view.selectedIndexes():
video = self.model.video_files[index.row()]
failure = failure or (
0 != subprocess.call([
"ln",
"-s",
video.fn,
os.path.join(directory, os.path.basename(video.fn)),
])
)
if failure:
QtWidgets.QMessageBox.warning(
self,
"Linkage failure",
"Some files could not be linked.")
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Ctrl+L"), self)
self.shortcut.activated.connect(link)
def delete():
from actions import Trash
selected = self.list_view.selectedIndexes()
response = QtWidgets.QMessageBox.question(self, "Delete Items", "Do you really want to delete %d items?" % len(selected), defaultButton=QtWidgets.QMessageBox.No)
if response == QtWidgets.QMessageBox.Yes:
actions = []
for i, index in enumerate(sorted(selected, key=lambda r: -r.row())):
video = self.model.video_files[index.row()]
action = Trash(video.fn, actions[-1] if len(actions) else None)
actions.append(action)
history.apply(self.model, *actions)
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("Del"), self)
self.shortcut.activated.connect(lambda : delete())
self.shortcut = QtWidgets.QShortcut(QtGui.QKeySequence("F5"), self)
self.shortcut.activated.connect(lambda : self.model.reload())
self.model = VideoModel()
def open_file(index):
self.OpenFile(self.model.video_files[index.row()].fn)
self.list_view = QtWidgets.QListView()
self.list_view.setSelectionMode(QtWidgets.QAbstractItemView.SelectionMode.ExtendedSelection)
self.list_view.doubleClicked.connect(open_file)
self.list_view.setViewMode(QtWidgets.QListView.IconMode)
self.list_view.setResizeMode(QtWidgets.QListView.Adjust)
self.list_view.setFlow(QtWidgets.QListView.LeftToRight)
self.list_view.setMovement(QtWidgets.QListView.Snap)
self.list_view.setModel(self.model)
# self.list_view.setGridSize(QtCore.QSize(256 + 10, 256 + 10))
self.icon_size = 256
self.list_view.setIconSize(QtCore.QSize(self.icon_size, self.icon_size))
self.list_view.setWordWrap(True)
self.hboxlayout = QtWidgets.QSplitter()
self.hboxlayout.addWidget(self.list_view)
self.hboxlayout.addWidget(self.videoplayerlayoutWidget)
self.filter_slider = QLabeledRangeSlider(QtCore.Qt.Horizontal)
self.filter_slider.setRange(0, 6)
self.filter_slider.setValue((0, 6))
self.filter_slider.setMaximum(6)
self.filter_slider.setMinimum(-1)
def set_rating_filter(v):
self.model.filter_rating = v
self.filter_slider.sliderMoved.connect(set_rating_filter)
self.parentLayout = QtWidgets.QVBoxLayout()
self.parentLayout.addWidget(self.filter_slider, 0)
self.parentLayout.addWidget(self.hboxlayout, 2)
self.widget.setLayout(self.parentLayout)
exit = QtWidgets.QAction("&Exit", self)
exit.triggered.connect(sys.exit)
menubar = self.menuBar()
filemenu = menubar.addMenu("&File")
filemenu.addAction(exit)
self.timer = QtCore.QTimer(self)
self.timer.setInterval(200)
self.timer.timeout.connect(self.updateUI)
def PlayPause(self):
"""Toggle play/pause status
"""
if self.mediaplayer.is_playing():
self.mediaplayer.pause()
self.playbutton.setText("Play")
self.isPaused = True
else:
if self.mediaplayer.play() == -1:
self.OpenFile()
return
self.mediaplayer.play()
self.playbutton.setText("Pause")
self.timer.start()
self.isPaused = False
def Stop(self):
"""Stop player
"""
self.mediaplayer.stop()
self.playbutton.setText("Play")
def OpenFile(self, filename=None):
"""Open a media file in a MediaPlayer
"""
if filename is None:
return
filename = QtWidgets.QFileDialog.getOpenFileName(self, "Open File", os.path.expanduser('~'))
if not filename:
return
# create the media
if sys.version < '3':
filename = unicode(filename)
self.filename = filename
self.media = self.instance.media_new(filename)
# put the media in the media player
self.mediaplayer.set_media(self.media)
# parse the metadata of the file
self.media.parse()
# set the title of the track as window title
self.setWindowTitle(self.media.get_meta(0))
# the media player has to be 'connected' to the QFrame
# (otherwise a video would be displayed in it's own window)
# this is platform specific!
# you have to give the id of the QFrame (or similar object) to
# vlc, different platforms have different functions for this
if sys.platform.startswith('linux'): # for Linux using the X Server
self.mediaplayer.set_xwindow(int(self.videoframe.winId()))
elif sys.platform == "win32": # for Windows
self.mediaplayer.set_hwnd(self.videoframe.winId())
elif sys.platform == "darwin": # for MacOS
self.mediaplayer.set_nsobject(self.videoframe.winId())
self.PlayPause()
def setVolume(self, Volume):
"""Set the volume
"""
self.mediaplayer.audio_set_volume(Volume)
def setPosition(self, position):
"""Set the position
"""
# setting the position to where the slider was dragged
self.mediaplayer.set_position(position / 1000.0)
# the vlc MediaPlayer needs a float value between 0 and 1, Qt
# uses integer variables, so you need a factor; the higher the
# factor, the more precise are the results
# (1000 should be enough)
def updateUI(self):
"""updates the user interface"""
# setting the slider to the desired position
self.positionslider.setValue(int(self.mediaplayer.get_position() * 1000))
if not self.mediaplayer.is_playing():
# no need to call this function if nothing is played
self.timer.stop()
if not self.isPaused:
# after the video finished, the play button stills shows
# "Pause", not the desired behavior of a media player
# this will fix it
self.Stop()
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
player = Player()
player.show()
player.resize(640, 480)
if sys.argv[1:]:
player.OpenFile(sys.argv[1])
sys.exit(app.exec_())