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