PoC: added intro time display and editing
This commit is contained in:
parent
8ebaa2798f
commit
3d3df85845
@ -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
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
25
app/music.py
25
app/music.py
@ -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"""
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
Reference in New Issue
Block a user