Implement intro timing and countdown

This commit is contained in:
Keith Edmunds 2024-05-10 18:01:41 +01:00
parent 0c03db14d4
commit 45a22c47d0
13 changed files with 6026 additions and 4007 deletions

View File

@ -152,6 +152,7 @@ class PlaylistTrack:
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.intro = track.intro
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id

View File

@ -52,6 +52,7 @@ class Config(object):
HEADER_BITRATE = "bps"
HEADER_DURATION = "Length"
HEADER_END_TIME = "End"
HEADER_INTRO = "Intro"
HEADER_LAST_PLAYED = "Last played"
HEADER_NOTE = "Notes"
HEADER_START_GAP = "Gap"
@ -59,6 +60,7 @@ class Config(object):
HEADER_TITLE = "Title"
HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15
INTRO_SECONDS_FORMAT = ".1f"
LAST_PLAYED_TODAY_STRING = "Today"
LAST_PLAYED_TOOLTIP_DATE_FORMAT = "%a, %d %b %Y"
LOG_LEVEL_STDERR = logging.INFO

View File

@ -157,6 +157,7 @@ class TracksTable(Model):
bitrate: Mapped[Optional[int]] = mapped_column(default=None)
duration: Mapped[int] = mapped_column(index=True)
fade_at: Mapped[int] = mapped_column(index=False)
intro: Mapped[Optional[int]] = mapped_column(default=None)
mtime: Mapped[float] = mapped_column(index=True)
path: Mapped[str] = mapped_column(String(2048), index=False, unique=True)
silence_at: Mapped[int] = mapped_column(index=False)

View File

@ -206,6 +206,20 @@ class Music:
self.player.set_position(position)
self.start_dt = dt.datetime.now()
# For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses
# Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more
# than once in the first 200mS.
for _ in range(3):
if self.player:
volume = self.player.audio_get_volume()
if volume < Config.VLC_VOLUME_DEFAULT:
self.set_volume(Config.VLC_VOLUME_DEFAULT)
log.error(f"Reset from {volume=}")
sleep(0.1)
def set_volume(self, volume=None, set_default=True) -> None:
"""Set maximum volume used for player"""

View File

@ -231,7 +231,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer1000: QTimer = QTimer()
self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.preview_player: music.Music = music.Music(name=Config.VLC_PREVIEW_PLAYER_NAME)
self.preview_player: music.Music = music.Music(
name=Config.VLC_PREVIEW_PLAYER_NAME
)
self.playing: bool = False
self.set_main_window_size()
@ -582,7 +584,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnHidePlayed.clicked.connect(self.hide_played)
self.btnPreviewBack.clicked.connect(self.preview_back)
self.btnPreview.clicked.connect(self.preview)
self.btnPreviewArm.clicked.connect(self.preview_arm)
self.btnPreviewFwd.clicked.connect(self.preview_fwd)
self.btnPreviewMark.clicked.connect(self.preview_mark)
self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next)
@ -1256,6 +1260,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.preview_player.stop()
self.label_intro_timer.setText("0.0")
def preview_arm(self):
"""Manager arm button for setting intro length"""
self.btnPreviewMark.setEnabled(self.btnPreviewArm.isChecked())
def preview_back(self) -> None:
"""Wind back preview file"""
@ -1266,6 +1275,20 @@ class Window(QMainWindow, Ui_MainWindow):
self.preview_player.move_forward(Config.PREVIEW_ADVANCE_MS)
def preview_mark(self) -> None:
"""Set intro time"""
track_id = self.active_tab().get_selected_row_track_id()
row_number = self.active_tab().get_selected_row()
if track_id:
with db.Session() as session:
track = session.get(Tracks, track_id)
if track:
track.intro = self.preview_player.get_playtime()
session.commit()
self.active_tab().source_model.refresh_row(session, row_number)
self.active_tab().source_model.invalidate_row(row_number)
def rename_playlist(self) -> None:
"""
Rename current playlist
@ -1689,6 +1712,19 @@ class Window(QMainWindow, Ui_MainWindow):
Called every 100ms
"""
# Update intro counter if applicable and, if updated, return
# because playing an intro takes precedence over timing a
# preview.
if (
self.music.is_playing()
and track_sequence.now.intro
):
remaining_ms = track_sequence.now.intro - self.music.get_playtime()
if remaining_ms > 0:
self.label_intro_timer.setText(f"{remaining_ms / 1000:.1f}")
return
# Ensure preview button is reset if preview finishes playing
self.btnPreview.setChecked(self.preview_player.is_playing())
@ -1697,6 +1733,9 @@ class Window(QMainWindow, Ui_MainWindow):
playtime = self.preview_player.get_playtime()
self.label_intro_timer.setText(f"{playtime / 1000:.1f}")
else:
self.label_intro_timer.setText("0.0")
def tick_1000ms(self) -> None:
"""
Called every 1000ms

View File

@ -52,6 +52,7 @@ class Col(Enum):
START_GAP = 0
TITLE = auto()
ARTIST = auto()
INTRO = auto()
DURATION = auto()
START_TIME = auto()
END_TIME = auto()
@ -69,6 +70,7 @@ class PlaylistRowData:
self.artist: str = ""
self.bitrate = 0
self.duration: int = 0
self.intro: Optional[int] = None
self.lastplayed: dt.datetime = Config.EPOCH
self.path = ""
self.played = False
@ -86,6 +88,7 @@ class PlaylistRowData:
self.title = plr.track.title
self.artist = plr.track.artist
self.duration = plr.track.duration
self.intro = plr.track.intro
self.played = plr.played
if plr.track.playdates:
self.lastplayed = max([a.lastplayed for a in plr.track.playdates])
@ -244,7 +247,7 @@ class PlaylistModel(QAbstractTableModel):
def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
"""Standard function for view"""
return 9
return len(Col)
def current_track_started(self) -> None:
"""
@ -442,14 +445,20 @@ class PlaylistModel(QAbstractTableModel):
return QVariant(end_time.strftime(Config.TRACK_TIME_FORMAT))
return QVariant()
if column == Col.INTRO.value:
if prd.intro:
return QVariant(f"{prd.intro / 1000:{Config.INTRO_SECONDS_FORMAT}}")
else:
return QVariant()
dispatch_table = {
Col.START_GAP.value: QVariant(prd.start_gap),
Col.TITLE.value: QVariant(prd.title),
Col.ARTIST.value: QVariant(prd.artist),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.DURATION.value: QVariant(ms_to_mmss(prd.duration)),
Col.LAST_PLAYED.value: QVariant(get_relative_date(prd.lastplayed)),
Col.BITRATE.value: QVariant(prd.bitrate),
Col.NOTE.value: QVariant(prd.note),
Col.START_GAP.value: QVariant(prd.start_gap),
Col.TITLE.value: QVariant(prd.title),
}
if column in dispatch_table:
return dispatch_table[column]
@ -574,6 +583,13 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number]
def get_row_track_id(self, row_number: int) -> Optional[int]:
"""
Return id of track associated with row or None if no track associated
"""
return self.playlist_rows[row_number].track_id
def get_row_track_path(self, row_number: int) -> str:
"""
Return path of track associated with row or empty string if no track associated
@ -619,6 +635,8 @@ class PlaylistModel(QAbstractTableModel):
if orientation == Qt.Orientation.Horizontal:
if section == Col.START_GAP.value:
return QVariant(Config.HEADER_START_GAP)
if section == Col.INTRO.value:
return QVariant(Config.HEADER_INTRO)
elif section == Col.TITLE.value:
return QVariant(Config.HEADER_TITLE)
elif section == Col.ARTIST.value:

View File

@ -600,6 +600,24 @@ class PlaylistTab(QTableView):
self.source_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection()
def get_selected_row_track_id(self) -> Optional[int]:
"""
Return the track_id of the selected row. If no row selected or selected
row does not have a track, return None.
"""
log.debug("get_selected_row_track_id() called")
model_row_number = self.source_model_selected_row_number()
if model_row_number is None:
result = None
else:
result = self.source_model.get_row_track_id(model_row_number)
log.debug(f"get_selected_row_track_id() returned: {result=}")
return result
def get_selected_row_track_path(self) -> str:
"""
Return the path of the selected row. If no row selected or selected
@ -617,6 +635,17 @@ class PlaylistTab(QTableView):
log.debug(f"get_selected_row_track_path() returned: {result=}")
return result
def get_selected_row(self) -> Optional[int]:
"""
Return selected row number. If no rows or multiple rows selected, return None
"""
selected = self.get_selected_rows()
if len(selected) == 1:
return selected[0]
else:
return None
def get_selected_rows(self) -> List[int]:
"""Return a list of model-selected row numbers sorted by row"""

View File

@ -1,5 +1,7 @@
<RCC>
<qresource prefix="icons">
<file>star.png</file>
<file>star_empty.png</file>
<file>record-red-button.png</file>
<file>record-button.png</file>
<file alias="headphones">headphone-symbol.png</file>

File diff suppressed because it is too large Load Diff

View File

@ -503,7 +503,7 @@ padding-left: 8px;</string>
<string>&lt;&lt;</string>
</property>
</widget>
<widget class="QPushButton" name="btnIntroArm">
<widget class="QPushButton" name="btnPreviewArm">
<property name="geometry">
<rect>
<x>44</x>
@ -587,6 +587,9 @@ padding-left: 8px;</string>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewMark">
<property name="enabled">
<bool>false</bool>
</property>
<property name="geometry">
<rect>
<x>44</x>
@ -608,7 +611,13 @@ padding-left: 8px;</string>
</size>
</property>
<property name="text">
<string>*</string>
<string/>
</property>
<property name="icon">
<iconset>
<normalon>:/icons/star.png</normalon>
<disabledoff>:/icons/star_empty.png</disabledoff>
</iconset>
</property>
</widget>
<widget class="QPushButton" name="btnPreviewFwd">

View File

@ -233,17 +233,17 @@ class Ui_MainWindow(object):
self.btnPreviewStart.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewStart.setObjectName("btnPreviewStart")
self.btnIntroArm = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnIntroArm.setGeometry(QtCore.QRect(44, 0, 44, 23))
self.btnIntroArm.setMinimumSize(QtCore.QSize(44, 23))
self.btnIntroArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnIntroArm.setText("")
self.btnPreviewArm = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewArm.setGeometry(QtCore.QRect(44, 0, 44, 23))
self.btnPreviewArm.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewArm.setText("")
icon2 = QtGui.QIcon()
icon2.addPixmap(QtGui.QPixmap(":/icons/record-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
icon2.addPixmap(QtGui.QPixmap(":/icons/record-red-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
self.btnIntroArm.setIcon(icon2)
self.btnIntroArm.setCheckable(True)
self.btnIntroArm.setObjectName("btnIntroArm")
self.btnPreviewArm.setIcon(icon2)
self.btnPreviewArm.setCheckable(True)
self.btnPreviewArm.setObjectName("btnPreviewArm")
self.btnPreviewEnd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewEnd.setGeometry(QtCore.QRect(88, 0, 44, 23))
self.btnPreviewEnd.setMinimumSize(QtCore.QSize(44, 23))
@ -255,9 +255,15 @@ class Ui_MainWindow(object):
self.btnPreviewBack.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewBack.setObjectName("btnPreviewBack")
self.btnPreviewMark = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewMark.setEnabled(False)
self.btnPreviewMark.setGeometry(QtCore.QRect(44, 23, 44, 23))
self.btnPreviewMark.setMinimumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23))
self.btnPreviewMark.setText("")
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/star.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On)
icon3.addPixmap(QtGui.QPixmap(":/icons/star_empty.png"), QtGui.QIcon.Mode.Disabled, QtGui.QIcon.State.Off)
self.btnPreviewMark.setIcon(icon3)
self.btnPreviewMark.setObjectName("btnPreviewMark")
self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls)
self.btnPreviewFwd.setGeometry(QtCore.QRect(88, 23, 44, 23))
@ -376,17 +382,17 @@ class Ui_MainWindow(object):
self.btnFade = QtWidgets.QPushButton(parent=self.frame)
self.btnFade.setMinimumSize(QtCore.QSize(132, 32))
self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215))
icon3 = QtGui.QIcon()
icon3.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon3)
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnFade.setIcon(icon4)
self.btnFade.setIconSize(QtCore.QSize(30, 30))
self.btnFade.setObjectName("btnFade")
self.verticalLayout_5.addWidget(self.btnFade)
self.btnStop = QtWidgets.QPushButton(parent=self.frame)
self.btnStop.setMinimumSize(QtCore.QSize(0, 36))
icon4 = QtGui.QIcon()
icon4.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon4)
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.btnStop.setIcon(icon5)
self.btnStop.setObjectName("btnStop")
self.verticalLayout_5.addWidget(self.btnStop)
self.horizontalLayout.addWidget(self.frame)
@ -410,41 +416,41 @@ class Ui_MainWindow(object):
self.statusbar.setObjectName("statusbar")
MainWindow.setStatusBar(self.statusbar)
self.actionPlay_next = QtGui.QAction(parent=MainWindow)
icon5 = QtGui.QIcon()
icon5.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon5)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionPlay_next.setIcon(icon6)
self.actionPlay_next.setObjectName("actionPlay_next")
self.actionSkipToNext = QtGui.QAction(parent=MainWindow)
icon6 = QtGui.QIcon()
icon6.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon6)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionSkipToNext.setIcon(icon7)
self.actionSkipToNext.setObjectName("actionSkipToNext")
self.actionInsertTrack = QtGui.QAction(parent=MainWindow)
icon7 = QtGui.QIcon()
icon7.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon7)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionInsertTrack.setIcon(icon8)
self.actionInsertTrack.setObjectName("actionInsertTrack")
self.actionAdd_file = QtGui.QAction(parent=MainWindow)
icon8 = QtGui.QIcon()
icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon8)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionAdd_file.setIcon(icon9)
self.actionAdd_file.setObjectName("actionAdd_file")
self.actionFade = QtGui.QAction(parent=MainWindow)
icon9 = QtGui.QIcon()
icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon9)
icon10 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionFade.setIcon(icon10)
self.actionFade.setObjectName("actionFade")
self.actionStop = QtGui.QAction(parent=MainWindow)
icon10 = QtGui.QIcon()
icon10.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon10)
icon11 = QtGui.QIcon()
icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.actionStop.setIcon(icon11)
self.actionStop.setObjectName("actionStop")
self.action_Clear_selection = QtGui.QAction(parent=MainWindow)
self.action_Clear_selection.setObjectName("action_Clear_selection")
self.action_Resume_previous = QtGui.QAction(parent=MainWindow)
icon11 = QtGui.QIcon()
icon11.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon11)
icon12 = QtGui.QIcon()
icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
self.action_Resume_previous.setIcon(icon12)
self.action_Resume_previous.setObjectName("action_Resume_previous")
self.actionE_xit = QtGui.QAction(parent=MainWindow)
self.actionE_xit.setObjectName("actionE_xit")
@ -590,7 +596,6 @@ class Ui_MainWindow(object):
self.btnPreviewStart.setText(_translate("MainWindow", "<<"))
self.btnPreviewEnd.setText(_translate("MainWindow", ">>"))
self.btnPreviewBack.setText(_translate("MainWindow", "<"))
self.btnPreviewMark.setText(_translate("MainWindow", "*"))
self.btnPreviewFwd.setText(_translate("MainWindow", ">"))
self.label_7.setText(_translate("MainWindow", "Intro"))
self.label_intro_timer.setText(_translate("MainWindow", "0:0"))

BIN
app/ui/star.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
app/ui/star_empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB