PoC: added intro time display and editing

This commit is contained in:
Keith Edmunds 2024-05-25 09:29:03 +01:00
parent 8ebaa2798f
commit 3d3df85845
6 changed files with 107 additions and 48 deletions

View File

@ -1,5 +1,6 @@
# Standard library imports # Standard library imports
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import auto, Enum
from typing import Any, Optional from typing import Any, Optional
import datetime as dt import datetime as dt
@ -18,6 +19,19 @@ from models import PlaylistRows
import helpers import helpers
class Col(Enum):
START_GAP = 0
TITLE = auto()
ARTIST = auto()
INTRO = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
LAST_PLAYED = auto()
BITRATE = auto()
NOTE = auto()
class FadeCurve: class FadeCurve:
GraphWidget = None GraphWidget = None

View File

@ -60,6 +60,7 @@ class Config(object):
HEADER_TITLE = "Title" HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
INTRO_END_GAP_MS = 1000
INTRO_SECONDS_FORMAT = ".1f" INTRO_SECONDS_FORMAT = ".1f"
INTRO_SECONDS_WARNING_MS = 3000 INTRO_SECONDS_WARNING_MS = 3000
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"

View File

@ -77,7 +77,7 @@ class Music:
if not position: if not position:
position = 0 position = 0
new_position = max(0, position + ((position * ms) / elapsed_ms)) new_position = max(0, position + ((position * ms) / elapsed_ms))
self.player.set_position(new_position) self.set_position(new_position)
# Adjus start time so elapsed time calculations are correct # Adjus start time so elapsed time calculations are correct
if new_position == 0: if new_position == 0:
self.start_dt = dt.datetime.now() self.start_dt = dt.datetime.now()
@ -125,21 +125,6 @@ class Music:
elapsed_seconds = (now - self.start_dt).total_seconds() elapsed_seconds = (now - self.start_dt).total_seconds()
return int(elapsed_seconds * 1000) return int(elapsed_seconds * 1000)
def get_playtime(self) -> int:
"""
Return number of milliseconds current track has been playing or
zero if not playing. The vlc function get_time() only updates 3-4
times a second; this function has much better resolution.
"""
if self.player is None or self.start_dt is None:
return 0
now = dt.datetime.now()
elapsed_time = now - self.start_dt
elapsed_seconds = elapsed_time.seconds + (elapsed_time.microseconds / 1000000)
return int(elapsed_seconds * 1000)
def get_position(self) -> Optional[float]: def get_position(self) -> Optional[float]:
"""Return current position""" """Return current position"""
@ -220,6 +205,14 @@ class Music:
log.error(f"Reset from {volume=}") log.error(f"Reset from {volume=}")
sleep(0.1) sleep(0.1)
def set_position(self, position: int) -> None:
"""
Set player position
"""
if self.player:
self.player.set_position(position)
def set_volume(self, volume=None, set_default=True) -> None: def set_volume(self, volume=None, set_default=True) -> None:
"""Set maximum volume used for player""" """Set maximum volume used for player"""

View File

@ -582,11 +582,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.clicked.connect(self.drop3db) self.btnDrop3db.clicked.connect(self.drop3db)
self.btnFade.clicked.connect(self.fade) self.btnFade.clicked.connect(self.fade)
self.btnHidePlayed.clicked.connect(self.hide_played) self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnPreviewArm.clicked.connect(self.preview_arm)
self.btnPreviewBack.clicked.connect(self.preview_back) self.btnPreviewBack.clicked.connect(self.preview_back)
self.btnPreview.clicked.connect(self.preview) self.btnPreview.clicked.connect(self.preview)
self.btnPreviewArm.clicked.connect(self.preview_arm) self.btnPreviewEnd.clicked.connect(self.preview_end)
self.btnPreviewFwd.clicked.connect(self.preview_fwd) self.btnPreviewFwd.clicked.connect(self.preview_fwd)
self.btnPreviewMark.clicked.connect(self.preview_mark) self.btnPreviewMark.clicked.connect(self.preview_mark)
self.btnPreviewStart.clicked.connect(self.preview_start)
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current) self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next) self.hdrNextTrack.clicked.connect(self.show_next)
@ -1259,6 +1261,7 @@ class Window(QMainWindow, Ui_MainWindow):
else: else:
self.preview_player.stop() self.preview_player.stop()
self.label_intro_timer.setText("0.0") self.label_intro_timer.setText("0.0")
self.btnPreviewMark.setEnabled(False)
def preview_arm(self): def preview_arm(self):
"""Manager arm button for setting intro length""" """Manager arm button for setting intro length"""
@ -1270,6 +1273,23 @@ class Window(QMainWindow, Ui_MainWindow):
self.preview_player.move_back(Config.PREVIEW_BACK_MS) self.preview_player.move_back(Config.PREVIEW_BACK_MS)
def preview_end(self) -> None:
"""Advance preview file to just before end of intro"""
return
# preview_track_path = self.preview_player.path
# if not preview_track_path:
# return
# with Session() as session:
# preview_track = Tracks.get_by_path(session, preview_track_path)
# if not preview_track or not preview_track.intro:
# return
# new_position = max(0, preview_track.intro - Config.INTRO_END_GAP_MS)
# self.preview_player.set_position(new_position)
def preview_fwd(self) -> None: def preview_fwd(self) -> None:
"""Advance preview file""" """Advance preview file"""
@ -1284,11 +1304,19 @@ class Window(QMainWindow, Ui_MainWindow):
with db.Session() as session: with db.Session() as session:
track = session.get(Tracks, track_id) track = session.get(Tracks, track_id)
if track: if track:
track.intro = self.preview_player.get_playtime() # Save intro as millisends rounded to nearest 0.1
# second because editor spinbox only resolves to 0.1
# seconds
track.intro = round(self.preview_player.get_playtime() / 100) * 100
session.commit() session.commit()
self.active_tab().source_model.refresh_row(session, row_number) self.active_tab().source_model.refresh_row(session, row_number)
self.active_tab().source_model.invalidate_row(row_number) self.active_tab().source_model.invalidate_row(row_number)
def preview_start(self) -> None:
"""Advance preview file"""
self.preview_player.set_position(0)
def rename_playlist(self) -> None: def rename_playlist(self) -> None:
""" """
Rename current playlist Rename current playlist
@ -1734,7 +1762,14 @@ class Window(QMainWindow, Ui_MainWindow):
if self.preview_player.is_playing(): if self.preview_player.is_playing():
playtime = self.preview_player.get_playtime() playtime = self.preview_player.get_playtime()
self.label_intro_timer.setText(f"{playtime / 1000:.1f}") self.label_intro_timer.setText(f"{playtime / 1000:.1f}")
if playtime <= 0:
self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
elif playtime <= Config.INTRO_SECONDS_WARNING_MS:
self.label_intro_timer.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
else: else:
self.label_intro_timer.setText("0.0") self.label_intro_timer.setText("0.0")

View File

@ -2,7 +2,6 @@
# Allow forward reference to PlaylistModel # Allow forward reference to PlaylistModel
from __future__ import annotations from __future__ import annotations
from enum import auto, Enum
from operator import attrgetter from operator import attrgetter
from random import shuffle from random import shuffle
from typing import List, Optional from typing import List, Optional
@ -31,7 +30,7 @@ import obswebsocket # type: ignore
# import snoop # type: ignore # import snoop # type: ignore
# App imports # App imports
from classes import track_sequence, MusicMusterSignals, PlaylistTrack from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack
from config import Config from config import Config
from helpers import ( from helpers import (
file_is_unreadable, file_is_unreadable,
@ -48,19 +47,6 @@ HEADER_NOTES_COLUMN = 1
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
class Col(Enum):
START_GAP = 0
TITLE = auto()
ARTIST = auto()
INTRO = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
LAST_PLAYED = auto()
BITRATE = auto()
NOTE = auto()
class PlaylistRowData: class PlaylistRowData:
def __init__(self, plr: PlaylistRows) -> None: def __init__(self, plr: PlaylistRows) -> None:
""" """
@ -490,6 +476,8 @@ class PlaylistModel(QAbstractTableModel):
if self.is_header_row(row) and column == HEADER_NOTES_COLUMN: if self.is_header_row(row) and column == HEADER_NOTES_COLUMN:
return QVariant(prd.note) return QVariant(prd.note)
if column == Col.INTRO.value:
return QVariant(prd.intro)
if column == Col.TITLE.value: if column == Col.TITLE.value:
return QVariant(prd.title) return QVariant(prd.title)
if column == Col.ARTIST.value: if column == Col.ARTIST.value:
@ -512,7 +500,12 @@ class PlaylistModel(QAbstractTableModel):
| Qt.ItemFlag.ItemIsSelectable | Qt.ItemFlag.ItemIsSelectable
| Qt.ItemFlag.ItemIsDragEnabled | Qt.ItemFlag.ItemIsDragEnabled
) )
if index.column() in [Col.TITLE.value, Col.ARTIST.value, Col.NOTE.value]: if index.column() in [
Col.TITLE.value,
Col.ARTIST.value,
Col.NOTE.value,
Col.INTRO.value,
]:
return default | Qt.ItemFlag.ItemIsEditable return default | Qt.ItemFlag.ItemIsEditable
return default return default
@ -1299,7 +1292,7 @@ class PlaylistModel(QAbstractTableModel):
self.update_track_times() self.update_track_times()
def setData( def setData(
self, index: QModelIndex, value: QVariant, role: int = Qt.ItemDataRole.EditRole self, index: QModelIndex, value: str | float, role: int = Qt.ItemDataRole.EditRole
) -> bool: ) -> bool:
""" """
Update model with edited data Update model with edited data
@ -1319,7 +1312,7 @@ class PlaylistModel(QAbstractTableModel):
return False return False
if plr.track_id: if plr.track_id:
if column == Col.TITLE.value or column == Col.ARTIST.value: if column in [Col.TITLE.value, Col.ARTIST.value, Col.INTRO.value]:
track = session.get(Tracks, plr.track_id) track = session.get(Tracks, plr.track_id)
if not track: if not track:
print(f"Error retreiving track: {plr=}") print(f"Error retreiving track: {plr=}")
@ -1328,11 +1321,14 @@ class PlaylistModel(QAbstractTableModel):
track.title = str(value) track.title = str(value)
elif column == Col.ARTIST.value: elif column == Col.ARTIST.value:
track.artist = str(value) track.artist = str(value)
elif column == Col.INTRO.value:
track.intro = int(round(float(value), 1) * 1000)
else: else:
print(f"Error updating track: {column=}, {value=}") print(f"Error updating track: {column=}, {value=}")
return False return False
elif column == Col.NOTE.value: elif column == Col.NOTE.value:
plr.note = str(value) plr.note = str(value)
else: else:
# This is a header row # This is a header row
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:

View File

@ -17,24 +17,25 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate, QAbstractItemDelegate,
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QDoubleSpinBox,
QHeaderView, QHeaderView,
QMenu, QMenu,
QMessageBox, QMessageBox,
QPlainTextEdit, QPlainTextEdit,
QProxyStyle,
QStyle,
QStyledItemDelegate, QStyledItemDelegate,
QStyleOption,
QStyleOptionViewItem, QStyleOptionViewItem,
QTableView, QTableView,
QTableWidgetItem, QTableWidgetItem,
QWidget, QWidget,
QProxyStyle,
QStyle,
QStyleOption,
) )
# Third party imports # Third party imports
# App imports # App imports
from classes import MusicMusterSignals, track_sequence from classes import Col, MusicMusterSignals, track_sequence
from config import Config from config import Config
from dialogs import TrackSelectDialog from dialogs import TrackSelectDialog
from helpers import ( from helpers import (
@ -73,15 +74,22 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller Intercept createEditor call and make row just a little bit taller
""" """
self.editor: QDoubleSpinBox | QPlainTextEdit
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.signals.enable_escape_signal.emit(False) self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab): if isinstance(self.parent(), PlaylistTab):
p = cast(PlaylistTab, self.parent()) p = cast(PlaylistTab, self.parent())
if isinstance(index.data(), str): if index.column() == Col.INTRO.value:
self.editor = QDoubleSpinBox(parent)
self.editor.setDecimals(1)
self.editor.setSingleStep(0.1)
return self.editor
elif isinstance(index.data(), str):
self.editor = QPlainTextEdit(parent)
row = index.row() row = index.row()
row_height = p.rowHeight(row) row_height = p.rowHeight(row)
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT) p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
self.editor = QPlainTextEdit(parent)
return self.editor return self.editor
return super().createEditor(parent, option, index) return super().createEditor(parent, option, index)
@ -107,10 +115,16 @@ class EscapeDelegate(QStyledItemDelegate):
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
if self.original_text == self.editor.toPlainText(): # Close editor if no changes have been made
# No changes made data_modified = False
if isinstance(self.editor, QPlainTextEdit):
data_modified = self.original_model_data == self.editor.toPlainText()
elif isinstance(self.editor, QDoubleSpinBox):
data_modified = self.original_model_data == int(self.editor.value()) * 1000
if data_modified:
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
) )
@ -123,16 +137,22 @@ class EscapeDelegate(QStyledItemDelegate):
proxy_model = index.model() proxy_model = index.model()
edit_index = proxy_model.mapToSource(index) edit_index = proxy_model.mapToSource(index)
self.original_text = self.source_model.data( self.original_model_data = self.source_model.data(
edit_index, Qt.ItemDataRole.EditRole edit_index, Qt.ItemDataRole.EditRole
) )
editor.setPlainText(self.original_text.value()) if index.column() == Col.INTRO.value:
editor.setValue(self.original_model_data.value() / 1000)
else:
editor.setPlainText(self.original_model_data.value())
def setModelData(self, editor, model, index): def setModelData(self, editor, model, index):
proxy_model = index.model() proxy_model = index.model()
edit_index = proxy_model.mapToSource(index) edit_index = proxy_model.mapToSource(index)
value = editor.toPlainText().strip() if isinstance(self.editor, QPlainTextEdit):
value = editor.toPlainText().strip()
elif isinstance(self.editor, QDoubleSpinBox):
value = editor.value()
self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole) self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index): def updateEditorGeometry(self, editor, option, index):