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
from dataclasses import dataclass, field
from enum import auto, Enum
from typing import Any, Optional
import datetime as dt
@ -18,6 +19,19 @@ from models import PlaylistRows
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:
GraphWidget = None

View File

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

View File

@ -77,7 +77,7 @@ class Music:
if not position:
position = 0
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
if new_position == 0:
self.start_dt = dt.datetime.now()
@ -125,21 +125,6 @@ class Music:
elapsed_seconds = (now - self.start_dt).total_seconds()
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]:
"""Return current position"""
@ -220,6 +205,14 @@ class Music:
log.error(f"Reset from {volume=}")
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:
"""Set maximum volume used for player"""

View File

@ -582,11 +582,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.clicked.connect(self.drop3db)
self.btnFade.clicked.connect(self.fade)
self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnPreviewArm.clicked.connect(self.preview_arm)
self.btnPreviewBack.clicked.connect(self.preview_back)
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.btnPreviewMark.clicked.connect(self.preview_mark)
self.btnPreviewStart.clicked.connect(self.preview_start)
self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next)
@ -1259,6 +1261,7 @@ class Window(QMainWindow, Ui_MainWindow):
else:
self.preview_player.stop()
self.label_intro_timer.setText("0.0")
self.btnPreviewMark.setEnabled(False)
def preview_arm(self):
"""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)
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:
"""Advance preview file"""
@ -1284,11 +1304,19 @@ class Window(QMainWindow, Ui_MainWindow):
with db.Session() as session:
track = session.get(Tracks, track_id)
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()
self.active_tab().source_model.refresh_row(session, 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:
"""
Rename current playlist
@ -1734,7 +1762,14 @@ class Window(QMainWindow, Ui_MainWindow):
if self.preview_player.is_playing():
playtime = self.preview_player.get_playtime()
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:
self.label_intro_timer.setText("0.0")

View File

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

View File

@ -17,24 +17,25 @@ from PyQt6.QtWidgets import (
QAbstractItemDelegate,
QAbstractItemView,
QApplication,
QDoubleSpinBox,
QHeaderView,
QMenu,
QMessageBox,
QPlainTextEdit,
QProxyStyle,
QStyle,
QStyledItemDelegate,
QStyleOption,
QStyleOptionViewItem,
QTableView,
QTableWidgetItem,
QWidget,
QProxyStyle,
QStyle,
QStyleOption,
)
# Third party imports
# App imports
from classes import MusicMusterSignals, track_sequence
from classes import Col, MusicMusterSignals, track_sequence
from config import Config
from dialogs import TrackSelectDialog
from helpers import (
@ -73,15 +74,22 @@ class EscapeDelegate(QStyledItemDelegate):
Intercept createEditor call and make row just a little bit taller
"""
self.editor: QDoubleSpinBox | QPlainTextEdit
self.signals = MusicMusterSignals()
self.signals.enable_escape_signal.emit(False)
if isinstance(self.parent(), PlaylistTab):
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_height = p.rowHeight(row)
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
self.editor = QPlainTextEdit(parent)
return self.editor
return super().createEditor(parent, option, index)
@ -107,10 +115,16 @@ class EscapeDelegate(QStyledItemDelegate):
self.closeEditor.emit(editor)
return True
elif key_event.key() == Qt.Key.Key_Escape:
if self.original_text == self.editor.toPlainText():
# No changes made
# Close editor if no changes have been 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)
return True
discard_edits = QMessageBox.question(
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
)
@ -123,16 +137,22 @@ class EscapeDelegate(QStyledItemDelegate):
proxy_model = index.model()
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
)
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):
proxy_model = index.model()
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)
def updateEditorGeometry(self, editor, option, index):