WIP: track ending signal

This commit is contained in:
Keith Edmunds 2025-04-14 19:18:26 +01:00
parent 5f0da55a24
commit 747f28f4f9
5 changed files with 172 additions and 199 deletions

View File

@ -291,7 +291,8 @@ class MusicMusterSignals(QObject):
# Dispay status message to user # Dispay status message to user
status_message_signal = pyqtSignal(str, int) status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal() # Emitted when track ends or is manually faded
track_ended_signal = pyqtSignal(int)
def __post_init__(self): def __post_init__(self):
super().__init__() super().__init__()

View File

@ -1063,6 +1063,9 @@ def playdates_update(track_id: int, when: dt.datetime | None = None) -> None:
Update playdates for passed track Update playdates for passed track
""" """
if not when:
when = dt.datetime.now()
with db.Session() as session: with db.Session() as session:
_ = Playdates(session, track_id, when) _ = Playdates(session, track_id, when)

View File

@ -3,7 +3,7 @@
# Standard library imports # Standard library imports
from __future__ import annotations from __future__ import annotations
from slugify import slugify # type: ignore from slugify import slugify # type: ignore
from typing import Callable, Optional from typing import Callable
import argparse import argparse
from dataclasses import dataclass from dataclasses import dataclass
import datetime as dt import datetime as dt
@ -72,24 +72,24 @@ from classes import (
TrackAndPlaylist, TrackAndPlaylist,
TrackInfo, TrackInfo,
) )
from config import Config from config import Config
from dialogs import TrackInsertDialog from dialogs import TrackInsertDialog
from file_importer import FileImporter from file_importer import FileImporter
from helpers import ask_yes_no, file_is_unreadable, get_name from helpers import ask_yes_no, file_is_unreadable, get_name
from log import log, log_call from log import log, log_call
from playlistrow import PlaylistRow, TrackSequence
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
from playlistrow import PlaylistRow, TrackSequence
from playlists import PlaylistTab from playlists import PlaylistTab
import ds
from querylistmodel import QuerylistModel from querylistmodel import QuerylistModel
from ui import icons_rc # noqa F401
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
from ui.downloadcsv_ui import Ui_DateSelect # type: ignore from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
from ui import icons_rc # noqa F401
from ui.main_window_footer_ui import Ui_FooterSection # type: ignore
from ui.main_window_header_ui import Ui_HeaderSection # type: ignore from ui.main_window_header_ui import Ui_HeaderSection # type: ignore
from ui.main_window_playlist_ui import Ui_PlaylistSection # type: ignore from ui.main_window_playlist_ui import Ui_PlaylistSection # type: ignore
from ui.main_window_footer_ui import Ui_FooterSection # type: ignore
from utilities import check_db, update_bitrates from utilities import check_db, update_bitrates
import ds
import helpers import helpers
@ -661,15 +661,6 @@ class ManageTemplates(ItemlistManager):
ds.playlist_rename(template_id, new_name) ds.playlist_rename(template_id, new_name)
@dataclass
class ItemlistManagerCallbacks:
delete: Callable[[int], None]
edit: Callable[[int], None]
favourite: Callable[[int, bool], None]
new_item: Callable[[], None]
rename: Callable[[int], Optional[str]]
class PreviewManager: class PreviewManager:
""" """
Manage track preview player Manage track preview player
@ -677,10 +668,10 @@ class PreviewManager:
def __init__(self) -> None: def __init__(self) -> None:
mixer.init() mixer.init()
self.intro: Optional[int] = None self.intro: int | None = None
self.path: str = "" self.path: str = ""
self.row_number: Optional[int] = None self.row_number: int | None = None
self.start_time: Optional[dt.datetime] = None self.start_time: dt.datetime | None = None
self.track_id: int = 0 self.track_id: int = 0
def back(self, ms: int) -> None: def back(self, ms: int) -> None:
@ -1036,7 +1027,7 @@ class TemplateSelectorDialog(QDialog):
""" """
def __init__( def __init__(
self, templates: list[tuple[str, int]], template_prompt: Optional[str] self, templates: list[tuple[str, int]], template_prompt: str | None
) -> None: ) -> None:
super().__init__() super().__init__()
self.templates = templates self.templates = templates
@ -1111,7 +1102,7 @@ class FooterSection(QWidget, Ui_FooterSection):
class Window(QMainWindow): class Window(QMainWindow):
def __init__( def __init__(
self, parent: Optional[QWidget] = None, *args: list, **kwargs: dict self, parent: QWidget | None = None, *args: list, **kwargs: dict
) -> None: ) -> None:
super().__init__(parent) super().__init__(parent)
@ -1128,45 +1119,48 @@ class Window(QMainWindow):
layout.addWidget(self.playlist_section) layout.addWidget(self.playlist_section)
layout.addWidget(self.footer_section) layout.addWidget(self.footer_section)
self.setWindowTitle(Config.MAIN_WINDOW_TITLE)
# Add menu bar
self.create_menu_bar()
self.timer10: QTimer = QTimer()
self.timer100: QTimer = QTimer()
self.timer500: QTimer = QTimer()
self.timer1000: QTimer = QTimer()
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
self.statusbar = self.statusBar()
if self.statusbar:
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
self.txtSearch = QLineEdit()
self.txtSearch.setHidden(True)
self.statusbar.addWidget(self.txtSearch)
self.hide_played_tracks = False
self.preview_manager = PreviewManager()
self.footer_section.widgetFadeVolume.hideAxis("bottom") self.footer_section.widgetFadeVolume.hideAxis("bottom")
self.footer_section.widgetFadeVolume.hideAxis("left") self.footer_section.widgetFadeVolume.hideAxis("left")
self.footer_section.widgetFadeVolume.setDefaultPadding(0) self.footer_section.widgetFadeVolume.setDefaultPadding(0)
self.footer_section.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND) self.footer_section.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
self.move_source: MoveSource | None = None self.setWindowTitle(Config.MAIN_WINDOW_TITLE)
self.disable_selection_timing = False # Add menu bar
self.clock_counter = 0 self.create_menu_bar()
# Configure main window
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
self.statusbar = self.statusBar()
if not self.statusbar:
raise ApplicationError("Can't create status bar")
self.statusbar.addPermanentWidget(self.lblSumPlaytime)
self.txtSearch = QLineEdit()
self.txtSearch.setHidden(True)
self.statusbar.addWidget(self.txtSearch)
self.hide_played_tracks = False
# Timers
self.timer10: QTimer = QTimer()
self.timer100: QTimer = QTimer()
self.timer500: QTimer = QTimer()
self.timer1000: QTimer = QTimer()
self.timer10.start(10) self.timer10.start(10)
self.timer100.start(100) self.timer100.start(100)
self.timer500.start(500) self.timer500.start(500)
self.timer1000.start(1000) self.timer1000.start(1000)
self.signals = MusicMusterSignals()
self.connect_signals_slots() # Misc
self.preview_manager = PreviewManager()
self.move_source: MoveSource | None = None
self.disable_selection_timing = False
self.catch_return_key = False self.catch_return_key = False
self.importer: Optional[FileImporter] = None self.importer: FileImporter | None = None
self.current = Current() self.current = Current()
self.track_sequence = TrackSequence() self.track_sequence = TrackSequence()
self.signals = MusicMusterSignals()
self.connect_signals_slots()
webbrowser.register( webbrowser.register(
"browser", "browser",
@ -1178,12 +1172,12 @@ class Window(QMainWindow):
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self) self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
self.action_quicklog.activated.connect(self.quicklog) self.action_quicklog.activated.connect(self.quicklog)
# Load playlists
self.load_last_playlists() self.load_last_playlists()
self.stop_autoplay = False
# # # # # # # # # # Overrides # # # # # # # # # # # # # # # # # # # # Overrides # # # # # # # # # #
def closeEvent(self, event: Optional[QCloseEvent]) -> None: def closeEvent(self, event: QCloseEvent | None) -> None:
"""Handle attempt to close main window""" """Handle attempt to close main window"""
if not event: if not event:
@ -1210,7 +1204,7 @@ class Window(QMainWindow):
mainwindow_width=self.width(), mainwindow_width=self.width(),
mainwindow_x=self.x(), mainwindow_x=self.x(),
mainwindow_y=self.y(), mainwindow_y=self.y(),
active_tab=self.playlist_section.tabPlaylist.currentIndex(), active_index=self.playlist_section.tabPlaylist.currentIndex(),
) )
for name, value in attributes_to_save.items(): for name, value in attributes_to_save.items():
ds.setting_set(name, value) ds.setting_set(name, value)
@ -1219,16 +1213,17 @@ class Window(QMainWindow):
# # # # # # # # # # Internal utility functions # # # # # # # # # # # # # # # # # # # # Internal utility functions # # # # # # # # # #
def active_tab(self) -> PlaylistTab: def _active_tab(self) -> PlaylistTab:
return self.playlist_section.tabPlaylist.currentWidget() return self.playlist_section.tabPlaylist.currentWidget()
# # # # # # # # # # Menu functions # # # # # # # # # # # # # # # # # # # # Menu functions # # # # # # # # # #
def create_action( def create_action(
self, text: str, handler: Callable, shortcut: Optional[str] = None self, text: str, handler: Callable, shortcut: str | None = None
) -> QAction: ) -> QAction:
""" """
Helper function to create an action, bind it to a method, and set a shortcut if provided. Helper function for menu creation. Create an action, bind it to a
method, and set a shortcut if provided.
""" """
action = QAction(text, self) action = QAction(text, self)
@ -1281,34 +1276,6 @@ class Window(QMainWindow):
menu.addAction(action) menu.addAction(action)
def populate_dynamic_submenu(self):
"""Dynamically populates submenus when they are selected."""
submenu = self.sender() # Get the submenu that triggered the event
# Find which submenu it is
for key, stored_submenu in self.dynamic_submenus.items():
if submenu == stored_submenu:
submenu.clear()
# Dynamically call the correct function
items = getattr(self, f"get_{key}_items")()
for item in items:
# Check for separator
if "separator" in item and item["separator"]:
submenu.addSeparator()
continue
action = QAction(item["text"], self)
# Extract handler and arguments
handler = getattr(self, item["handler"], None)
args = item.get("args", ())
if handler:
# Use a lambda to pass arguments to the function
action.triggered.connect(lambda _, h=handler, a=args: h(a))
submenu.addAction(action)
break
def get_new_playlist_dynamic_submenu_items( def get_new_playlist_dynamic_submenu_items(
self, self,
) -> list[dict[str, str | int | bool]]: ) -> list[dict[str, str | int | bool]]:
@ -1376,41 +1343,33 @@ class Window(QMainWindow):
return submenu_items return submenu_items
def show_query(self, query_id: int) -> None: def populate_dynamic_submenu(self):
""" """Dynamically populates submenus when they are selected."""
Show query dialog with query_id selected submenu = self.sender() # Get the submenu that triggered the event
"""
# Keep a reference else it will be gc'd # Find which submenu it is
self.query_dialog = QueryDialog(query_id) for key, stored_submenu in self.dynamic_submenus.items():
if self.query_dialog.exec(): if submenu == stored_submenu:
new_row_number = self.current_row_or_end() submenu.clear()
base_model = self.current.base_model # Dynamically call the correct function
for track_id in self.query_dialog.selected_tracks: items = getattr(self, f"get_{key}_items")()
# Check whether track is already in playlist for item in items:
move_existing = False # Check for separator
existing_prd = base_model.is_track_in_playlist(track_id) if "separator" in item and item["separator"]:
if existing_prd is not None: submenu.addSeparator()
if ask_yes_no( continue
"Duplicate row", action = QAction(item["text"], self)
"Track already in playlist. " "Move to new location?",
default_yes=True,
):
move_existing = True
if move_existing and existing_prd: # Extract handler and arguments
base_model.move_track_add_note( handler = getattr(self, item["handler"], None)
new_row_number, existing_prd, note="" args = item.get("args", ())
)
else: if handler:
self.signals.signal_insert_track.emit( # Use a lambda to pass arguments to the function
InsertTrack( action.triggered.connect(lambda _, h=handler, a=args: h(a))
playlist_id=base_model.playlist_id,
track_id=track_id, submenu.addAction(action)
note="", break
)
)
new_row_number += 1
# # # # # # # # # # Playlist management functions # # # # # # # # # # # # # # # # # # # # Playlist management functions # # # # # # # # # #
@ -1418,7 +1377,7 @@ class Window(QMainWindow):
def _create_playlist(self, name: str, template_id: int) -> PlaylistDTO: def _create_playlist(self, name: str, template_id: int) -> PlaylistDTO:
""" """
Create a playlist in the database, populate it from the template Create a playlist in the database, populate it from the template
if template_id > 0, and return the Playlists object. if template_id > 0, and return the PlaylistDTO object.
""" """
return ds.playlist_create(name, template_id) return ds.playlist_create(name, template_id)
@ -1455,7 +1414,7 @@ class Window(QMainWindow):
# @log_call # @log_call
def create_playlist_from_template(self, template_id: int) -> None: def create_playlist_from_template(self, template_id: int) -> None:
""" """
Prompt for new playlist name and create from passed template_id Prompt for new playlist name and create from passed template_id.
""" """
if template_id == 0: if template_id == 0:
@ -1463,7 +1422,6 @@ class Window(QMainWindow):
selected_template_id = self.solicit_template_to_use() selected_template_id = self.solicit_template_to_use()
if selected_template_id is None: if selected_template_id is None:
return return
else:
template_id = selected_template_id template_id = selected_template_id
playlist_name = self.get_playlist_name() playlist_name = self.get_playlist_name()
@ -1526,7 +1484,7 @@ class Window(QMainWindow):
def get_playlist_name( def get_playlist_name(
self, default: str = "", prompt: str = "Playlist name:" self, default: str = "", prompt: str = "Playlist name:"
) -> Optional[str]: ) -> str | None:
"""Get a name from the user""" """Get a name from the user"""
dlg = QInputDialog(self) dlg = QInputDialog(self)
@ -1554,8 +1512,8 @@ class Window(QMainWindow):
return None return None
def solicit_template_to_use( def solicit_template_to_use(
self, template_prompt: Optional[str] = None self, template_prompt: str | None = None
) -> Optional[int]: ) -> int | None:
""" """
Have user select a template. Return the template.id, or None if they cancel. Have user select a template. Return the template.id, or None if they cancel.
template_id of zero means don't use a template. template_id of zero means don't use a template.
@ -1589,12 +1547,48 @@ class Window(QMainWindow):
_ = ManageTemplates(self) _ = ManageTemplates(self)
def show_query(self, query_id: int) -> None:
"""
Show query dialog with query_id selected
"""
# Keep a reference else it will be gc'd
self.query_dialog = QueryDialog(query_id)
if self.query_dialog.exec():
new_row_number = self.current_row_or_end()
base_model = self.current.base_model
for track_id in self.query_dialog.selected_tracks:
# Check whether track is already in playlist
move_existing = False
existing_prd = base_model.is_track_in_playlist(track_id)
if existing_prd is not None:
if ask_yes_no(
"Duplicate row",
"Track already in playlist. " "Move to new location?",
default_yes=True,
):
move_existing = True
if move_existing and existing_prd:
base_model.move_track_add_note(
new_row_number, existing_prd, note=""
)
else:
self.signals.signal_insert_track.emit(
InsertTrack(
playlist_id=base_model.playlist_id,
track_id=track_id,
note="",
)
)
new_row_number += 1
# # # # # # # # # # Miscellaneous functions # # # # # # # # # # # # # # # # # # # # Miscellaneous functions # # # # # # # # # #
def select_duplicate_rows(self, checked: bool = False) -> None: def select_duplicate_rows(self, checked: bool = False) -> None:
"""Call playlist to select duplicate rows""" """Call playlist to select duplicate rows"""
self.active_tab().select_duplicate_rows() self._active_tab().select_duplicate_rows()
def about(self, checked: bool = False) -> None: def about(self, checked: bool = False) -> None:
"""Get git tag and database name""" """Get git tag and database name"""
@ -1621,14 +1615,14 @@ class Window(QMainWindow):
""" """
self.track_sequence.set_next(None) self.track_sequence.set_next(None)
self.update_headers() self.signals.next_track_changed_signal.emit()
def clear_selection(self, checked: bool = False) -> None: def clear_selection(self, checked: bool = False) -> None:
"""Clear row selection""" """Clear row selection"""
# Unselect any selected rows # Unselect any selected rows
if self.active_tab(): if self._active_tab():
self.active_tab().clear_selection() self._active_tab().clear_selection()
# Clear the search bar # Clear the search bar
self.search_playlist_clear() self.search_playlist_clear()
@ -1782,6 +1776,7 @@ class Window(QMainWindow):
# @log_call # @log_call
def end_of_track_actions(self) -> None: def end_of_track_actions(self) -> None:
""" """
Called by track_ended_signal
Actions required: Actions required:
- Reset track_sequence objects - Reset track_sequence objects
@ -1794,10 +1789,6 @@ class Window(QMainWindow):
if self.track_sequence.current: if self.track_sequence.current:
self.track_sequence.move_current_to_previous() self.track_sequence.move_current_to_previous()
# Tell playlist previous track has finished
# TODO: it should just catch track_ended_signal
self.current.base_model.previous_track_ended()
# Reset clocks # Reset clocks
self.footer_section.frame_fade.setStyleSheet("") self.footer_section.frame_fade.setStyleSheet("")
self.footer_section.frame_silent.setStyleSheet("") self.footer_section.frame_silent.setStyleSheet("")
@ -1811,10 +1802,6 @@ class Window(QMainWindow):
self.catch_return_key = False self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0) self.show_status_message("Play controls: Enabled", 0)
# autoplay
# if not self.stop_autoplay:
# self.play_next()
def export_playlist_tab(self, checked: bool = False) -> None: def export_playlist_tab(self, checked: bool = False) -> None:
"""Export the current playlist to an m3u file""" """Export the current playlist to an m3u file"""
@ -1859,7 +1846,7 @@ class Window(QMainWindow):
if self.track_sequence.current: if self.track_sequence.current:
self.track_sequence.current.fade() self.track_sequence.current.fade()
def get_tab_index_for_playlist(self, playlist_id: int) -> Optional[int]: def get_tab_index_for_playlist(self, playlist_id: int) -> int | None:
""" """
Return the tab index for the passed playlist_id if it is displayed, Return the tab index for the passed playlist_id if it is displayed,
else return None. else return None.
@ -1882,12 +1869,12 @@ class Window(QMainWindow):
self.hide_played_tracks = True self.hide_played_tracks = True
self.footer_section.btnHidePlayed.setText("Show played") self.footer_section.btnHidePlayed.setText("Show played")
if Config.HIDE_PLAYED_MODE == Config.HIDE_PLAYED_MODE_SECTIONS: if Config.HIDE_PLAYED_MODE == Config.HIDE_PLAYED_MODE_SECTIONS:
self.active_tab().hide_played_sections() self._active_tab().hide_played_sections()
else: else:
self.current.base_model.hide_played_tracks(True) self.current.base_model.hide_played_tracks(True)
# Reset row heights # Reset row heights
self.active_tab().resize_rows() self._active_tab().resize_rows()
def import_files_wrapper(self, checked: bool = False) -> None: def import_files_wrapper(self, checked: bool = False) -> None:
""" """
@ -1920,7 +1907,7 @@ class Window(QMainWindow):
def insert_track(self, checked: bool = False) -> None: def insert_track(self, checked: bool = False) -> None:
"""Show dialog box to select and add track from database""" """Show dialog box to select and add track from database"""
dlg = TrackInsertDialog(parent=self, playlist_id=self.active_tab().playlist_id) dlg = TrackInsertDialog(parent=self, playlist_id=self.current.playlist_id)
dlg.exec() dlg.exec()
# @log_call # @log_call
@ -1934,7 +1921,7 @@ class Window(QMainWindow):
playlist_ids.append(self._open_playlist(playlist)) playlist_ids.append(self._open_playlist(playlist))
# Set active tab # Set active tab
value = ds.setting_get("active_tab") value = ds.setting_get("active_index")
if value is not None and value >= 0: if value is not None and value >= 0:
self.playlist_section.tabPlaylist.setCurrentIndex(value) self.playlist_section.tabPlaylist.setCurrentIndex(value)
@ -1969,7 +1956,7 @@ class Window(QMainWindow):
# paste # paste
self.move_source = MoveSource( self.move_source = MoveSource(
model=self.current.base_model, model=self.current.base_model,
rows=[a.row_number for a in self.current.base_model.selected_rows], rows=self.current.selected_row_numbers
) )
log.debug(f"mark_rows_for_moving(): {self.move_source=}") log.debug(f"mark_rows_for_moving(): {self.move_source=}")
@ -1980,6 +1967,9 @@ class Window(QMainWindow):
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
""" """
if not row_numbers:
return
# Identify destination playlist # Identify destination playlist
playlists = [] playlists = []
source_playlist_id = self.current.playlist_id source_playlist_id = self.current.playlist_id
@ -2013,11 +2003,7 @@ class Window(QMainWindow):
Move selected rows to another playlist Move selected rows to another playlist
""" """
selected_rows = self.current.selected_row_numbers self.move_playlist_rows(self.current.selected_row_numbers)
if not selected_rows:
return
self.move_playlist_rows(selected_rows)
def move_unplayed(self, checked: bool = False) -> None: def move_unplayed(self, checked: bool = False) -> None:
""" """
@ -2074,8 +2060,8 @@ class Window(QMainWindow):
from_rows, to_row, to_playlist_model.playlist_id from_rows, to_row, to_playlist_model.playlist_id
) )
self.active_tab().resize_rows() self._active_tab().resize_rows()
self.active_tab().clear_selection() self._active_tab().clear_selection()
# If we move a row to immediately under the current track, make # If we move a row to immediately under the current track, make
# that moved row the next track # that moved row the next track
@ -2088,7 +2074,7 @@ class Window(QMainWindow):
# @log_call # @log_call
def play_next( def play_next(
self, position: Optional[float] = None, checked: bool = False self, position: float | None = None, checked: bool = False
) -> None: ) -> None:
""" """
Play next track, optionally from passed position. Play next track, optionally from passed position.
@ -2118,7 +2104,7 @@ class Window(QMainWindow):
return return
# Issue #223 concerns a very short pause (maybe 0.1s) sometimes # Issue #223 concerns a very short pause (maybe 0.1s) sometimes
# when starting to play at track. Resolution appears to be to # just after a track starts playing. Resolution appears to be to
# disable timer10 for a short time. Timer is re-enabled in # disable timer10 for a short time. Timer is re-enabled in
# update_clocks. # update_clocks.
@ -2129,9 +2115,11 @@ class Window(QMainWindow):
if self.track_sequence.current: if self.track_sequence.current:
self.track_sequence.current.fade() self.track_sequence.current.fade()
# Move next track to current track. # Move next track to current track. end_of_track_actions() will
# end_of_track_actions() will have saved current track to # have been called when previous track ended or when fade() was
# called above, and that in turn will have saved current track to
# previous_track # previous_track
self.track_sequence.move_next_to_current() self.track_sequence.move_next_to_current()
if self.track_sequence.current is None: if self.track_sequence.current is None:
raise ApplicationError("No current track") raise ApplicationError("No current track")
@ -2159,6 +2147,9 @@ class Window(QMainWindow):
self.catch_return_key = True self.catch_return_key = True
self.show_status_message("Play controls: Disabled", 0) self.show_status_message("Play controls: Disabled", 0)
# Record playdate
ds.playdates_update(self.track_sequence.current.track_id)
# Notify others # Notify others
self.signals.signal_track_started.emit( self.signals.signal_track_started.emit(
TrackAndPlaylist( TrackAndPlaylist(
@ -2167,21 +2158,6 @@ class Window(QMainWindow):
) )
) )
# TODO: ensure signal_track_started does all this:
# self.active_tab().current_track_started()
# Update playdates
# Set toolips for hdrPreviousTrack (but let's do that dynamically
# on hover in future)
# with s-e-s-s-i-o-n:
# last_played = Playdates.last_played_tracks(s-e-s-s-i-o-n)
# tracklist = []
# for lp in last_played:
# track = s-e-s-s-i-o-n.get(Tracks, lp.track_id)
# tracklist.append(f"{track.title} ({track.artist})")
# tt = "<br>".join(tracklist)
# self.header_section.hdrPreviousTrack.setToolTip(tt)
# Update headers # Update headers
self.update_headers() self.update_headers()
@ -2194,7 +2170,7 @@ class Window(QMainWindow):
if self.footer_section.btnPreview.isChecked(): if self.footer_section.btnPreview.isChecked():
# Get track path for first selected track if there is one # Get track path for first selected track if there is one
track_info = self.active_tab().get_selected_row_track_info() track_info = self._active_tab().get_selected_row_track_info()
if not track_info: if not track_info:
# Otherwise get track_id to next track to play # Otherwise get track_id to next track to play
if self.track_sequence.next: if self.track_sequence.next:
@ -2203,10 +2179,9 @@ class Window(QMainWindow):
self.track_sequence.next.track_id, self.track_sequence.next.track_id,
self.track_sequence.next.row_number, self.track_sequence.next.row_number,
) )
else:
return
if not track_info: if not track_info:
return return
self.preview_manager.row_number = track_info.row_number self.preview_manager.row_number = track_info.row_number
track = ds.track_by_id(track_info.track_id) track = ds.track_by_id(track_info.track_id)
if not track: if not track:
@ -2394,7 +2369,7 @@ class Window(QMainWindow):
# Disable play controls so that 'return' in search box doesn't # Disable play controls so that 'return' in search box doesn't
# play next track # play next track
self.catch_return_key = True self.catch_return_key = True
self.txtSearch.setHidden(False) self.txtSearch.setVisible(True)
self.txtSearch.setFocus() self.txtSearch.setFocus()
# Select any text that may already be there # Select any text that may already be there
self.txtSearch.selectAll() self.txtSearch.selectAll()
@ -2413,13 +2388,13 @@ class Window(QMainWindow):
self.current.proxy_model.set_incremental_search(self.txtSearch.text()) self.current.proxy_model.set_incremental_search(self.txtSearch.text())
def selected_or_next_track_info(self) -> Optional[PlaylistRow]: def selected_or_next_track_info(self) -> PlaylistRow | None:
""" """
Return RowAndTrack info for selected track. If no selected track, return for Return RowAndTrack info for selected track. If no selected track, return for
next track. If no next track, return None. next track. If no next track, return None.
""" """
row_number: Optional[int] = None row_number: int | None = None
if self.current.selected_row_numbers: if self.current.selected_row_numbers:
row_number = self.current.selected_row_numbers[0] row_number = self.current.selected_row_numbers[0]
@ -2453,20 +2428,6 @@ class Window(QMainWindow):
self.signals.signal_set_next_row.emit(self.current.playlist_id) self.signals.signal_set_next_row.emit(self.current.playlist_id)
self.clear_selection() self.clear_selection()
# playlist_tab = self.active_tab()
# if playlist_tab:
# playlist_tab.set_row_as_next_track()
# else:
# log.error("No active tab")
# @log_call
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
"""
Find the tab containing the widget and set the text colour
"""
idx = self.playlist_section.tabPlaylist.indexOf(widget)
self.playlist_section.tabPlaylist.tabBar().setTabTextColor(idx, colour)
def show_current(self) -> None: def show_current(self) -> None:
"""Scroll to show current track""" """Scroll to show current track"""
@ -2476,10 +2437,9 @@ class Window(QMainWindow):
def show_warning(self, title: str, body: str) -> None: def show_warning(self, title: str, body: str) -> None:
""" """
Display a warning dialog Handle show_warning_signal and display a warning dialog
""" """
print(f"show_warning({title=}, {body=})")
QMessageBox.warning(self, title, body) QMessageBox.warning(self, title, body)
def show_next(self) -> None: def show_next(self) -> None:
@ -2490,6 +2450,7 @@ class Window(QMainWindow):
def show_status_message(self, message: str, timing: int) -> None: def show_status_message(self, message: str, timing: int) -> None:
""" """
Handle status_message_signal.
Show status message in status bar for timing milliseconds Show status message in status bar for timing milliseconds
Clear message if message is null string Clear message if message is null string
""" """
@ -2501,7 +2462,7 @@ class Window(QMainWindow):
self.statusbar.clearMessage() self.statusbar.clearMessage()
def show_track(self, playlist_track: PlaylistRow) -> None: def show_track(self, playlist_track: PlaylistRow) -> None:
"""Scroll to show track in plt""" """Scroll to show track"""
# Switch to the correct tab # Switch to the correct tab
playlist_id = playlist_track.playlist_id playlist_id = playlist_track.playlist_id
@ -2519,20 +2480,19 @@ class Window(QMainWindow):
f"show_track() can't find current playlist tab {playlist_id=}" f"show_track() can't find current playlist tab {playlist_id=}"
) )
self.active_tab().scroll_to_top(playlist_track.row_number) self._active_tab().scroll_to_top(playlist_track.row_number)
# @log_call # @log_call
def stop(self, checked: bool = False) -> None: def stop(self, checked: bool = False) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
self.stop_autoplay = True
if self.track_sequence.current: if self.track_sequence.current:
self.track_sequence.current.stop() self.track_sequence.current.stop()
def tab_change(self) -> None: def tab_change(self) -> None:
"""Called when active tab changed""" """Called when active tab changed"""
self.active_tab().tab_live() self._active_tab().tab_live()
def tick_10ms(self) -> None: def tick_10ms(self) -> None:
""" """
@ -2551,9 +2511,11 @@ class Window(QMainWindow):
try: try:
self.track_sequence.current.check_for_end_of_track() self.track_sequence.current.check_for_end_of_track()
# Update intro counter if applicable and, if updated, return # Update intro counter if applicable and, if updated,
# because playing an intro takes precedence over timing a # return because playing an intro uses the intro field to
# show timing and this takes precedence over timing a
# preview. # preview.
intro_ms_remaining = self.track_sequence.current.time_remaining_intro() intro_ms_remaining = self.track_sequence.current.time_remaining_intro()
if intro_ms_remaining > 0: if intro_ms_remaining > 0:
self.footer_section.label_intro_timer.setText( self.footer_section.label_intro_timer.setText(
@ -2572,7 +2534,6 @@ class Window(QMainWindow):
# current track ended during servicing tick # current track ended during servicing tick
pass pass
# Ensure preview button is reset if preview finishes playing
# Update preview timer # Update preview timer
if self.footer_section.btnPreview.isChecked(): if self.footer_section.btnPreview.isChecked():
if self.preview_manager.is_playing(): if self.preview_manager.is_playing():
@ -2584,6 +2545,8 @@ class Window(QMainWindow):
f"{int(minutes)}:{seconds:04.1f}" f"{int(minutes)}:{seconds:04.1f}"
) )
else: else:
# Ensure preview button is reset if preview has finished
# playing
self.footer_section.btnPreview.setChecked(False) self.footer_section.btnPreview.setChecked(False)
self.footer_section.label_intro_timer.setText("0.0") self.footer_section.label_intro_timer.setText("0.0")
self.footer_section.label_intro_timer.setStyleSheet("") self.footer_section.label_intro_timer.setStyleSheet("")

View File

@ -100,6 +100,7 @@ class PlaylistModel(QAbstractTableModel):
self.signals.signal_playlist_selected_rows.connect(self.set_selected_rows) self.signals.signal_playlist_selected_rows.connect(self.set_selected_rows)
self.signals.signal_set_next_row.connect(self.set_next_row) self.signals.signal_set_next_row.connect(self.set_next_row)
self.signals.signal_track_started.connect(self.track_started) self.signals.signal_track_started.connect(self.track_started)
self.signals.track_ended_signal.connect(self.previous_track_ended)
# Populate self.playlist_rows # Populate self.playlist_rows
for dto in ds.playlistrows_by_playlist(self.playlist_id): for dto in ds.playlistrows_by_playlist(self.playlist_id):
@ -267,7 +268,7 @@ class PlaylistModel(QAbstractTableModel):
track_id = play_track.track_id track_id = play_track.track_id
# Sanity check - 1 # Sanity check - 1
if not track_id: if not track_id:
raise ApplicationError("current_track_started() called with no track_id") raise ApplicationError("track_started() called with no track_id")
# Sanity check - 2 # Sanity check - 2
if self.track_sequence.current is None: if self.track_sequence.current is None:
@ -960,15 +961,19 @@ class PlaylistModel(QAbstractTableModel):
return return
# @log_call # @log_call
def previous_track_ended(self) -> None: def previous_track_ended(self, playlist_id: int) -> None:
""" """
Notification from musicmuster that the previous track has ended. Notification from track_ended_signal that the previous track has ended.
Actions required: Actions required:
- sanity check - sanity check
- update display - update display
""" """
if playlist_id != self.playlist_id:
# Not for us
return
# Sanity check # Sanity check
if not self.track_sequence.previous: if not self.track_sequence.previous:
log.error( log.error(

View File

@ -203,7 +203,7 @@ class PlaylistRow:
self.fade_graph.clear() self.fade_graph.clear()
# Ensure that player is released # Ensure that player is released
self.music.fade(0) self.music.fade(0)
self.signals.track_ended_signal.emit() self.signals.track_ended_signal.emit(self.playlist_id)
self.end_of_track_signalled = True self.end_of_track_signalled = True
def drop3db(self, enable: bool) -> None: def drop3db(self, enable: bool) -> None:
@ -221,7 +221,8 @@ class PlaylistRow:
self.resume_marker = self.music.get_position() self.resume_marker = self.music.get_position()
self.music.fade(fade_seconds) self.music.fade(fade_seconds)
self.signals.track_ended_signal.emit() self.signals.track_ended_signal.emit(self.playlist_id)
self.end_of_track_signalled = True
def is_playing(self) -> bool: def is_playing(self) -> bool:
""" """