Compare commits

..

9 Commits

Author SHA1 Message Date
Keith Edmunds
d6bb3d04d8 Row edit updates now handled in PlaylistRow 2025-04-15 10:22:10 +01:00
Keith Edmunds
a0ded4b73d Handle signal_insert_track better. 2025-04-15 10:21:26 +01:00
Keith Edmunds
6496ea2ac4 Don't close when track playing; mark as played in playlist 2025-04-14 21:23:00 +01:00
Keith Edmunds
c61df17dd5 Mark playlist rows played in db 2025-04-14 20:09:14 +01:00
Keith Edmunds
747f28f4f9 WIP: track ending signal 2025-04-14 19:18:26 +01:00
Keith Edmunds
5f0da55a24 Remove old ui files 2025-04-14 12:52:59 +01:00
Keith Edmunds
498923c3b3 Connect up signal_insert_track 2025-04-14 09:45:06 +01:00
Keith Edmunds
b34e0a014a WIP: more database updates; all tests run 2025-04-13 19:49:05 +01:00
Keith Edmunds
f9c33120f5 Document and clean up signals 2025-04-13 18:05:00 +01:00
12 changed files with 431 additions and 1914 deletions

View File

@ -218,6 +218,12 @@ class InsertTrack:
note: str note: str
@dataclass
class SelectedRows:
playlist_id: int
rows: list[int]
@dataclass @dataclass
class TrackAndPlaylist: class TrackAndPlaylist:
playlist_id: int playlist_id: int
@ -233,26 +239,60 @@ class MusicMusterSignals(QObject):
- https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class - https://stackoverflow.com/questions/62654525/emit-a-signal-from-another-class-to-main-class
""" """
# Used to en/disable escape as a shortcut key to "clear selection".
# We disable it when editing a field in the playlist because we use
# escape there to abandon an edit.
enable_escape_signal = pyqtSignal(bool) enable_escape_signal = pyqtSignal(bool)
# Signals that the next-cued track has changed. Used to update
# playlist headers.
next_track_changed_signal = pyqtSignal() next_track_changed_signal = pyqtSignal()
# Signals that the playlist_id passed should resize all rows.
resize_rows_signal = pyqtSignal(int) resize_rows_signal = pyqtSignal(int)
# Signal to open browser at songfacts or wikipedia page matching
# passed string.
search_songfacts_signal = pyqtSignal(str) search_songfacts_signal = pyqtSignal(str)
search_wikipedia_signal = pyqtSignal(str) search_wikipedia_signal = pyqtSignal(str)
# Displays a warning dialog
show_warning_signal = pyqtSignal(str, str) show_warning_signal = pyqtSignal(str, str)
signal_add_track_to_header = pyqtSignal(int)
# Signal to add a track to a header row
signal_add_track_to_header = pyqtSignal(TrackAndPlaylist)
# Signal to receving model that rows will be / have been inserter
signal_begin_insert_rows = pyqtSignal(InsertRows) signal_begin_insert_rows = pyqtSignal(InsertRows)
signal_end_insert_rows = pyqtSignal(int) signal_end_insert_rows = pyqtSignal(int)
# TBD
signal_insert_track = pyqtSignal(InsertTrack) signal_insert_track = pyqtSignal(InsertTrack)
signal_playlist_selected_rows = pyqtSignal(int, list)
# Keep track of which rows are selected (between playlist and model)
signal_playlist_selected_rows = pyqtSignal(SelectedRows)
# Signal to model that selected row is to be next row
signal_set_next_row = pyqtSignal(int) signal_set_next_row = pyqtSignal(int)
# signal_set_next_track takes a PlaylistRow as an argument. We can't # signal_set_next_track takes a PlaylistRow as an argument. We can't
# specify that here as it requires us to import PlaylistRow from # specify that here as it requires us to import PlaylistRow from
# playlistrow.py, which itself imports MusicMusterSignals # playlistrow.py, which itself imports MusicMusterSignals
# TBD
signal_set_next_track = pyqtSignal(object) signal_set_next_track = pyqtSignal(object)
# Emited when a track starts playing
signal_track_started = pyqtSignal(TrackAndPlaylist) signal_track_started = pyqtSignal(TrackAndPlaylist)
# Used by model to signal spanning of cells to playlist for headers
span_cells_signal = pyqtSignal(int, int, int, int, int) span_cells_signal = pyqtSignal(int, int, int, int, int)
# 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

@ -434,21 +434,6 @@ def tracks_filtered(filter: Filter) -> list[TrackDTO]:
return results return results
def track_set_intro(track_id: int, intro: int) -> None:
"""
Set track intro time
"""
with db.Session() as session:
session.execute(
update(Tracks)
.where(Tracks.id == track_id)
.values(intro=intro)
)
session.commit()
# @log_call # @log_call
def track_update( def track_update(
track_id: int, metadata: dict[str, str | int | float] track_id: int, metadata: dict[str, str | int | float]
@ -918,7 +903,6 @@ def playlist_remove_rows(playlist_id: int, row_numbers: list[int]) -> None:
) )
# Fixup row number to remove gaps # Fixup row number to remove gaps
_playlist_check_playlist(session, playlist_id, fix=True) _playlist_check_playlist(session, playlist_id, fix=True)
session.commit() session.commit()
@ -1029,6 +1013,38 @@ def playlistrows_by_playlist(
return dto_list return dto_list
def playlistrow_update_note(playlistrow_id: int, note: str) -> PlaylistRowDTO:
"""
Update the note on a playlist row
"""
with db.Session() as session:
plr = session.get(PlaylistRows, playlistrow_id)
if not plr:
raise ApplicationError(f"Can't retrieve Playlistrow ({playlistrow_id=})")
plr.note = note
session.commit()
new_plr = playlistrow_by_id(playlistrow_id)
if not new_plr:
raise ApplicationError(f"Can't retrieve new Playlistrow ({playlistrow_id=})")
return new_plr
def playlistrow_played(playlistrow_id: int, status: bool) -> None:
"""Update played status of row"""
with db.Session() as session:
session.execute(
update(PlaylistRows).where(PlaylistRows.id == playlistrow_id).values(played=status)
)
session.commit()
# Playdates # Playdates
# @log_call # @log_call
def playdates_get_last(track_id: int, limit: int = 5) -> str: def playdates_get_last(track_id: int, limit: int = 5) -> str:
@ -1058,6 +1074,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
@ -65,30 +65,31 @@ import stackprinter # type: ignore
from classes import ( from classes import (
ApplicationError, ApplicationError,
Filter, Filter,
InsertTrack,
MusicMusterSignals, MusicMusterSignals,
PlaylistDTO, PlaylistDTO,
QueryDTO, QueryDTO,
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
@ -660,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
@ -676,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:
@ -1035,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
@ -1110,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)
@ -1127,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",
@ -1177,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:
@ -1209,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)
@ -1218,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)
@ -1280,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]]:
@ -1375,36 +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:
base_model.insert_row(track_id)
new_row_number += 1 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
# # # # # # # # # # Playlist management functions # # # # # # # # # # # # # # # # # # # # Playlist management functions # # # # # # # # # #
@ -1412,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)
@ -1449,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:
@ -1457,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()
@ -1520,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)
@ -1548,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.
@ -1583,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"""
@ -1615,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()
@ -1776,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
@ -1788,9 +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
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("")
@ -1804,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"""
@ -1852,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.
@ -1875,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:
""" """
@ -1902,12 +1896,18 @@ class Window(QMainWindow):
dlg.resize(500, 100) dlg.resize(500, 100)
ok = dlg.exec() ok = dlg.exec()
if ok: if ok:
self.current.base_model.insert_row(note=dlg.textValue()) self.signals.signal_insert_track.emit(
InsertTrack(
playlist_id=self.current.base_model.playlist_id,
track_id=None,
note=dlg.textValue()
)
)
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
@ -1921,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)
@ -1956,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=}")
@ -1967,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
@ -2000,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:
""" """
@ -2061,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
@ -2075,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.
@ -2105,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.
@ -2116,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")
@ -2146,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(
@ -2154,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()
@ -2181,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:
@ -2190,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:
@ -2247,7 +2235,7 @@ class Window(QMainWindow):
return return
intro = round(self.preview_manager.get_playtime() / 100) * 100 intro = round(self.preview_manager.get_playtime() / 100) * 100
ds.track_set_intro(track_id, intro) ds.track_update(track_id, dict(intro=intro))
self.preview_manager.set_intro(intro) self.preview_manager.set_intro(intro)
self.current.base_model.refresh_row(row_number) self.current.base_model.refresh_row(row_number)
roles = [ roles = [
@ -2381,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()
@ -2400,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]
@ -2440,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"""
@ -2463,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:
@ -2477,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
""" """
@ -2488,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
@ -2506,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:
""" """
@ -2538,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(
@ -2559,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():
@ -2571,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

@ -36,6 +36,7 @@ from classes import (
InsertRows, InsertRows,
InsertTrack, InsertTrack,
MusicMusterSignals, MusicMusterSignals,
SelectedRows,
TrackAndPlaylist, TrackAndPlaylist,
) )
from config import Config from config import Config
@ -95,9 +96,11 @@ class PlaylistModel(QAbstractTableModel):
self.signals.signal_add_track_to_header.connect(self.add_track_to_header) self.signals.signal_add_track_to_header.connect(self.add_track_to_header)
self.signals.signal_begin_insert_rows.connect(self.begin_insert_rows) self.signals.signal_begin_insert_rows.connect(self.begin_insert_rows)
self.signals.signal_end_insert_rows.connect(self.end_insert_rows) self.signals.signal_end_insert_rows.connect(self.end_insert_rows)
self.signals.signal_insert_track.connect(self.insert_row_signal_handler)
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):
@ -265,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:
@ -294,7 +297,9 @@ class PlaylistModel(QAbstractTableModel):
# Update previous row in case we're hiding played rows # Update previous row in case we're hiding played rows
if self.track_sequence.previous and self.track_sequence.previous.row_number: if self.track_sequence.previous and self.track_sequence.previous.row_number:
# only invalidate required roles # only invalidate required roles
self.invalidate_row(self.track_sequence.previous.row_number, roles_to_invalidate) self.invalidate_row(
self.track_sequence.previous.row_number, roles_to_invalidate
)
# Update all other track times # Update all other track times
self.update_track_times() self.update_track_times()
@ -677,11 +682,14 @@ class PlaylistModel(QAbstractTableModel):
self.invalidate_row(row_number, roles) self.invalidate_row(row_number, roles)
# @log_call # @log_call
def insert_row(self, track_id: Optional[int] = None, note: str = "",) -> None: def insert_row_signal_handler(self, row_data: InsertTrack) -> None:
""" """
Insert a row. Handle the signal_insert_track signal
""" """
if row_data.playlist_id != self.playlist_id:
return
new_row_number = self._get_new_row_number() new_row_number = self._get_new_row_number()
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
@ -689,8 +697,8 @@ class PlaylistModel(QAbstractTableModel):
_ = ds.playlist_insert_row( _ = ds.playlist_insert_row(
playlist_id=self.playlist_id, playlist_id=self.playlist_id,
row_number=new_row_number, row_number=new_row_number,
track_id=track_id, track_id=row_data.track_id,
note=note, note=row_data.note,
) )
# Need to refresh self.playlist_rows because row numbers will have # Need to refresh self.playlist_rows because row numbers will have
# changed # changed
@ -796,14 +804,16 @@ class PlaylistModel(QAbstractTableModel):
if self.track_sequence.current: if self.track_sequence.current:
current_row = self.track_sequence.current.row_number current_row = self.track_sequence.current.row_number
if current_row in from_rows: if current_row in from_rows:
log.debug( log.debug("move_rows: Removing {current_row=} from {from_rows=}")
"move_rows: Removing {current_row=} from {from_rows=}"
)
from_rows.remove(self.track_sequence.current.row_number) from_rows.remove(self.track_sequence.current.row_number)
from_rows = sorted(set(from_rows)) from_rows = sorted(set(from_rows))
if (min(from_rows) < 0 or max(from_rows) >= self.rowCount() if (
or to_row_number < 0 or to_row_number > self.rowCount()): min(from_rows) < 0
or max(from_rows) >= self.rowCount()
or to_row_number < 0
or to_row_number > self.rowCount()
):
log.debug("move_rows: invalid indexes") log.debug("move_rows: invalid indexes")
return False return False
@ -861,15 +871,16 @@ class PlaylistModel(QAbstractTableModel):
# Prepare source model # Prepare source model
super().beginRemoveRows(QModelIndex(), min(row_group), max(row_group)) super().beginRemoveRows(QModelIndex(), min(row_group), max(row_group))
# Prepare destination model # Prepare destination model
insert_rows = InsertRows(to_playlist_id, insert_rows = InsertRows(
to_row_number, to_playlist_id, to_row_number, to_row_number + len(row_group)
to_row_number + len(row_group)
) )
self.signals.signal_begin_insert_rows.emit(insert_rows) self.signals.signal_begin_insert_rows.emit(insert_rows)
ds.playlist_move_rows(from_rows=row_group, ds.playlist_move_rows(
from_rows=row_group,
from_playlist_id=self.playlist_id, from_playlist_id=self.playlist_id,
to_row=to_row_number, to_row=to_row_number,
to_playlist_id=to_playlist_id) to_playlist_id=to_playlist_id,
)
self.signals.signal_end_insert_rows.emit(to_playlist_id) self.signals.signal_end_insert_rows.emit(to_playlist_id)
super().endRemoveRows() super().endRemoveRows()
@ -953,15 +964,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(
@ -1013,7 +1028,9 @@ class PlaylistModel(QAbstractTableModel):
plrid = self.playlist_rows[row_number].playlistrow_id plrid = self.playlist_rows[row_number].playlistrow_id
refreshed_row = ds.playlistrow_by_id(plrid) refreshed_row = ds.playlistrow_by_id(plrid)
if not refreshed_row: if not refreshed_row:
raise ApplicationError(f"Failed to retrieve row {self.playlist_id=}, {row_number=}") raise ApplicationError(
f"Failed to retrieve row {self.playlist_id=}, {row_number=}"
)
self.playlist_rows[row_number] = PlaylistRow(refreshed_row) self.playlist_rows[row_number] = PlaylistRow(refreshed_row)
@ -1230,16 +1247,16 @@ class PlaylistModel(QAbstractTableModel):
return True return True
# @log_call # @log_call
def set_selected_rows(self, playlist_id: int, selected_row_numbers: list[int]) -> None: def set_selected_rows(self, selected_rows: SelectedRows) -> None:
""" """
Handle signal_playlist_selected_rows to keep track of which rows Handle signal_playlist_selected_rows to keep track of which rows
are selected in the view are selected in the view
""" """
if playlist_id != self.playlist_id: if selected_rows.playlist_id != self.playlist_id:
return return
self.selected_rows = [self.playlist_rows[a] for a in selected_row_numbers] self.selected_rows = [self.playlist_rows[a] for a in selected_rows.rows]
# @log_call # @log_call
def set_next_row(self, playlist_id: int) -> None: def set_next_row(self, playlist_id: int) -> None:
@ -1247,9 +1264,7 @@ class PlaylistModel(QAbstractTableModel):
Handle signal_set_next_row Handle signal_set_next_row
""" """
log.debug(f"{self}: set_next_row({playlist_id=})")
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
# Not for us
return return
if len(self.selected_rows) == 0: if len(self.selected_rows) == 0:
@ -1294,7 +1309,10 @@ class PlaylistModel(QAbstractTableModel):
role: int = Qt.ItemDataRole.EditRole, role: int = Qt.ItemDataRole.EditRole,
) -> bool: ) -> bool:
""" """
Update model with edited data Update model with edited data. Here we simply update the
playlist_row in self.playlist_rows. The act of doing that will
trigger a database update in the @setter property in the
PlaylistRow class.
""" """
if not index.isValid() or role != Qt.ItemDataRole.EditRole: if not index.isValid() or role != Qt.ItemDataRole.EditRole:
@ -1304,14 +1322,19 @@ class PlaylistModel(QAbstractTableModel):
column = index.column() column = index.column()
plr = self.playlist_rows[row_number] plr = self.playlist_rows[row_number]
if column == Col.TITLE.value: if column == Col.NOTE.value:
plr.title = str(value) plr.note = value
elif column == Col.TITLE.value:
plr.title = value
elif column == Col.ARTIST.value: elif column == Col.ARTIST.value:
plr.artist = str(value) plr.artist = value
elif column == Col.INTRO.value: elif column == Col.INTRO.value:
plr.intro = int(round(float(value), 1) * 1000) intro = int(round(float(value), 1) * 1000)
elif column == Col.NOTE.value: plr.intro = intro
plr.note = str(value)
else: else:
raise ApplicationError(f"setData called with unexpected column ({column=})") raise ApplicationError(f"setData called with unexpected column ({column=})")
@ -1445,7 +1468,9 @@ class PlaylistModel(QAbstractTableModel):
] ]
self.invalidate_rows(track_rows, roles) self.invalidate_rows(track_rows, roles)
else: else:
self.insert_row(track_id=track_id) self.insert_row_signal_handler(
InsertTrack(playlist_id=self.playlist_id, track_id=track_id, note="")
)
# @log_call # @log_call
def update_track_times(self) -> None: def update_track_times(self) -> None:
@ -1471,7 +1496,10 @@ class PlaylistModel(QAbstractTableModel):
update_rows, self.track_sequence.current.start_time update_rows, self.track_sequence.current.start_time
) )
if self.track_sequence.next and self.track_sequence.next.playlist_id == self.playlist_id: if (
self.track_sequence.next
and self.track_sequence.next.playlist_id == self.playlist_id
):
next_track_row = self.track_sequence.next.row_number next_track_row = self.track_sequence.next.row_number
for row_number in range(row_count): for row_number in range(row_count):
@ -1576,15 +1604,22 @@ class PlaylistProxyModel(QSortFilterProxyModel):
): ):
# This row isn't our previous track: hide it # This row isn't our previous track: hide it
return False return False
if self.track_sequence.current and self.track_sequence.current.start_time: if (
self.track_sequence.current
and self.track_sequence.current.start_time
):
# This row is our previous track. Don't hide it # This row is our previous track. Don't hide it
# until HIDE_AFTER_PLAYING_OFFSET milliseconds # until HIDE_AFTER_PLAYING_OFFSET milliseconds
# after current track has started # after current track has started
if self.track_sequence.current.start_time and dt.datetime.now() > ( if (
self.track_sequence.current.start_time
and dt.datetime.now()
> (
self.track_sequence.current.start_time self.track_sequence.current.start_time
+ dt.timedelta( + dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
) )
)
): ):
return False return False
else: else:

View File

@ -71,8 +71,12 @@ class PlaylistRow:
return "" return ""
@artist.setter @artist.setter
def artist(self, value: str) -> None: def artist(self, artist: str) -> None:
print(f"set artist attribute for {self=}, {value=}") if not self.dto.track:
raise ApplicationError(f"No track_id when trying to set artist ({self})")
self.dto.track.artist = artist
ds.track_update(self.track_id, dict(artist=str(artist)))
@property @property
def bitrate(self): def bitrate(self):
@ -103,8 +107,12 @@ class PlaylistRow:
return 0 return 0
@intro.setter @intro.setter
def intro(self, value: int) -> None: def intro(self, intro: int) -> None:
print(f"set intro attribute for {self=}, {value=}") if not self.dto.track:
raise ApplicationError(f"No track_id when trying to set intro ({self})")
self.dto.track.intro = intro
ds.track_update(self.track_id, dict(intro=str(intro)))
@property @property
def lastplayed(self): def lastplayed(self):
@ -142,8 +150,12 @@ class PlaylistRow:
return "" return ""
@title.setter @title.setter
def title(self, value: str) -> None: def title(self, title: str) -> None:
print(f"set title attribute for {self=}, {value=}") if not self.dto.track:
raise ApplicationError(f"No track_id when trying to set title ({self})")
self.dto.track.title = title
ds.track_update(self.track_id, dict(title=str(title)))
@property @property
def track_id(self): def track_id(self):
@ -175,20 +187,18 @@ class PlaylistRow:
return self.dto.note return self.dto.note
@note.setter @note.setter
def note(self, value: str) -> None: def note(self, note: str) -> None:
# TODO set up write access to db self.dto.note = note
print(f"set note attribute for {self=}, {value=}") ds.playlistrow_update_note(self.playlistrow_id, str(note))
# self.dto.note = value
@property @property
def played(self): def played(self):
return self.dto.played return self.dto.played
@played.setter @played.setter
def played(self, value: bool = True) -> None: def played(self, value: bool) -> None:
# TODO set up write access to db self.dto.played = True
print(f"set played attribute for {self=}") ds.playlistrow_played(self.playlistrow_id, value)
# self.dto.played = value
@property @property
def playlist_id(self): def playlist_id(self):
@ -204,7 +214,8 @@ class PlaylistRow:
@row_number.setter @row_number.setter
def row_number(self, value: int) -> None: def row_number(self, value: int) -> None:
# TODO do we need to set up write access to db? # This does not update the database which must take place
# elsewhere
self.dto.row_number = value self.dto.row_number = value
def check_for_end_of_track(self) -> None: def check_for_end_of_track(self) -> None:
@ -226,7 +237,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:
@ -244,7 +255,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:
""" """

View File

@ -40,6 +40,7 @@ from classes import (
Col, Col,
MusicMusterSignals, MusicMusterSignals,
PlaylistStyle, PlaylistStyle,
SelectedRows,
TrackAndPlaylist, TrackAndPlaylist,
TrackInfo TrackInfo
) )
@ -471,7 +472,9 @@ class PlaylistTab(QTableView):
selected_row_numbers = self.get_selected_rows() selected_row_numbers = self.get_selected_rows()
# Signal selected rows to model # Signal selected rows to model
self.signals.signal_playlist_selected_rows.emit(self.playlist_id, selected_row_numbers) self.signals.signal_playlist_selected_rows.emit(
SelectedRows(self.playlist_id, selected_row_numbers)
)
# Put sum of selected tracks' duration in status bar # Put sum of selected tracks' duration in status bar
# If no rows are selected, we have nothing to do # If no rows are selected, we have nothing to do

View File

@ -1,131 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>584</width>
<height>377</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Title:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="searchString"/>
</item>
<item row="1" column="0" colspan="2">
<widget class="QListWidget" name="matchList"/>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="lblNote">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>46</width>
<height>16777215</height>
</size>
</property>
<property name="text">
<string>&amp;Note:</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="buddy">
<cstring>txtNote</cstring>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="txtNote"/>
</item>
</layout>
</item>
<item row="3" column="0" colspan="2">
<widget class="QLabel" name="dbPath">
<property name="text">
<string/>
</property>
</widget>
</item>
<item row="4" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QRadioButton" name="radioTitle">
<property name="text">
<string>&amp;Title</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioArtist">
<property name="text">
<string>&amp;Artist</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="btnAdd">
<property name="text">
<string>&amp;Add</string>
</property>
<property name="default">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnAddClose">
<property name="text">
<string>A&amp;dd and close</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="btnClose">
<property name="text">
<string>&amp;Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,83 +0,0 @@
# Form implementation generated from reading ui file 'dlg_TrackSelect.ui'
#
# Created by: PyQt6 UI code generator 6.5.3
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt6 import QtCore, QtGui, QtWidgets
class Ui_Dialog(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(584, 377)
self.gridLayout = QtWidgets.QGridLayout(Dialog)
self.gridLayout.setObjectName("gridLayout")
self.label = QtWidgets.QLabel(parent=Dialog)
self.label.setObjectName("label")
self.gridLayout.addWidget(self.label, 0, 0, 1, 1)
self.searchString = QtWidgets.QLineEdit(parent=Dialog)
self.searchString.setObjectName("searchString")
self.gridLayout.addWidget(self.searchString, 0, 1, 1, 1)
self.matchList = QtWidgets.QListWidget(parent=Dialog)
self.matchList.setObjectName("matchList")
self.gridLayout.addWidget(self.matchList, 1, 0, 1, 2)
self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setObjectName("horizontalLayout")
self.lblNote = QtWidgets.QLabel(parent=Dialog)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.lblNote.sizePolicy().hasHeightForWidth())
self.lblNote.setSizePolicy(sizePolicy)
self.lblNote.setMaximumSize(QtCore.QSize(46, 16777215))
self.lblNote.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeading|QtCore.Qt.AlignmentFlag.AlignLeft|QtCore.Qt.AlignmentFlag.AlignTop)
self.lblNote.setObjectName("lblNote")
self.horizontalLayout.addWidget(self.lblNote)
self.txtNote = QtWidgets.QLineEdit(parent=Dialog)
self.txtNote.setObjectName("txtNote")
self.horizontalLayout.addWidget(self.txtNote)
self.gridLayout.addLayout(self.horizontalLayout, 2, 0, 1, 2)
self.dbPath = QtWidgets.QLabel(parent=Dialog)
self.dbPath.setText("")
self.dbPath.setObjectName("dbPath")
self.gridLayout.addWidget(self.dbPath, 3, 0, 1, 2)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.radioTitle = QtWidgets.QRadioButton(parent=Dialog)
self.radioTitle.setChecked(True)
self.radioTitle.setObjectName("radioTitle")
self.horizontalLayout_2.addWidget(self.radioTitle)
self.radioArtist = QtWidgets.QRadioButton(parent=Dialog)
self.radioArtist.setObjectName("radioArtist")
self.horizontalLayout_2.addWidget(self.radioArtist)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum)
self.horizontalLayout_2.addItem(spacerItem)
self.btnAdd = QtWidgets.QPushButton(parent=Dialog)
self.btnAdd.setDefault(True)
self.btnAdd.setObjectName("btnAdd")
self.horizontalLayout_2.addWidget(self.btnAdd)
self.btnAddClose = QtWidgets.QPushButton(parent=Dialog)
self.btnAddClose.setObjectName("btnAddClose")
self.horizontalLayout_2.addWidget(self.btnAddClose)
self.btnClose = QtWidgets.QPushButton(parent=Dialog)
self.btnClose.setObjectName("btnClose")
self.horizontalLayout_2.addWidget(self.btnClose)
self.gridLayout.addLayout(self.horizontalLayout_2, 4, 0, 1, 2)
self.lblNote.setBuddy(self.txtNote)
self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)
def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Title:"))
self.lblNote.setText(_translate("Dialog", "&Note:"))
self.radioTitle.setText(_translate("Dialog", "&Title"))
self.radioArtist.setText(_translate("Dialog", "&Artist"))
self.btnAdd.setText(_translate("Dialog", "&Add"))
self.btnAddClose.setText(_translate("Dialog", "A&dd and close"))
self.btnClose.setText(_translate("Dialog", "&Close"))

File diff suppressed because it is too large Load Diff

View File

@ -91,6 +91,6 @@ def update_bitrates() -> None:
for track in ds.tracks_all(): for track in ds.tracks_all():
try: try:
t = get_tags(track.path) t = get_tags(track.path)
ds.track_update(t) ds.track_update(track.track_id, t._asdict())
except FileNotFoundError: except FileNotFoundError:
continue continue

View File

@ -11,6 +11,7 @@ from app.helpers import get_all_track_metadata
from app import ds, playlistmodel from app import ds, playlistmodel
from app.models import db from app.models import db
from classes import ( from classes import (
InsertTrack,
TrackAndPlaylist, TrackAndPlaylist,
) )
@ -39,7 +40,13 @@ class TestMMMiscTracks(unittest.TestCase):
track_path = self.test_tracks[row % len(self.test_tracks)] track_path = self.test_tracks[row % len(self.test_tracks)]
metadata = get_all_track_metadata(track_path) metadata = get_all_track_metadata(track_path)
track = ds.track_create(metadata) track = ds.track_create(metadata)
self.model.insert_row(track_id=track.track_id, note=f"{row=}") self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=track.track_id,
note=f"{row=}",
)
)
def tearDown(self): def tearDown(self):
db.drop_all() db.drop_all()
@ -59,9 +66,21 @@ class TestMMMiscTracks(unittest.TestCase):
# Fake selected row in model # Fake selected row in model
self.model.selected_rows = [self.model.playlist_rows[START_ROW]] self.model.selected_rows = [self.model.playlist_rows[START_ROW]]
self.model.insert_row(note="start+") self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note="start+"
)
)
self.model.selected_rows = [self.model.playlist_rows[END_ROW]] self.model.selected_rows = [self.model.playlist_rows[END_ROW]]
self.model.insert_row(note="-") self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note="-+"
)
)
prd = self.model.playlist_rows[START_ROW] prd = self.model.playlist_rows[START_ROW]
qv_value = self.model._display_role( qv_value = self.model._display_role(
@ -99,7 +118,13 @@ class TestMMMiscNoPlaylist(unittest.TestCase):
track_path = self.test_tracks[0] track_path = self.test_tracks[0]
metadata = get_all_track_metadata(track_path) metadata = get_all_track_metadata(track_path)
track = ds.track_create(metadata) track = ds.track_create(metadata)
model.insert_row(track_id=track.track_id) model.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist.playlist_id,
track_id=track.track_id,
note="",
)
)
prd = model.playlist_rows[model.rowCount() - 1] prd = model.playlist_rows[model.rowCount() - 1]
# test repr # test repr
@ -121,7 +146,13 @@ class TestMMMiscRowMove(unittest.TestCase):
self.playlist = ds.playlist_create(self.PLAYLIST_NAME, template_id=0) self.playlist = ds.playlist_create(self.PLAYLIST_NAME, template_id=0)
self.model = playlistmodel.PlaylistModel(self.playlist.playlist_id, is_template=False) self.model = playlistmodel.PlaylistModel(self.playlist.playlist_id, is_template=False)
for row in range(self.ROWS_TO_CREATE): for row in range(self.ROWS_TO_CREATE):
self.model.insert_row(note=str(row)) self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=str(row),
)
)
def tearDown(self): def tearDown(self):
db.drop_all() db.drop_all()
@ -132,7 +163,13 @@ class TestMMMiscRowMove(unittest.TestCase):
note_text = "test text" note_text = "test text"
assert self.model.rowCount() == self.ROWS_TO_CREATE assert self.model.rowCount() == self.ROWS_TO_CREATE
self.model.insert_row(note=note_text) self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
prd = self.model.playlist_rows[self.model.rowCount() - 1] prd = self.model.playlist_rows[self.model.rowCount() - 1]
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
@ -153,7 +190,13 @@ class TestMMMiscRowMove(unittest.TestCase):
# Fake selected row in model # Fake selected row in model
self.model.selected_rows = [self.model.playlist_rows[insert_row]] self.model.selected_rows = [self.model.playlist_rows[insert_row]]
self.model.insert_row(note=note_text) self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
prd = self.model.playlist_rows[insert_row] prd = self.model.playlist_rows[insert_row]
# Test against edit_role because display_role for headers is # Test against edit_role because display_role for headers is
@ -169,7 +212,13 @@ class TestMMMiscRowMove(unittest.TestCase):
note_text = "test text" note_text = "test text"
insert_row = 6 insert_row = 6
self.model.insert_row(note=note_text) self.model.insert_row_signal_handler(
InsertTrack(
playlist_id=self.playlist.playlist_id,
track_id=None,
note=note_text
)
)
assert self.model.rowCount() == self.ROWS_TO_CREATE + 1 assert self.model.rowCount() == self.ROWS_TO_CREATE + 1
# Fake selected row in model # Fake selected row in model
@ -206,7 +255,13 @@ class TestMMMiscRowMove(unittest.TestCase):
playlist_dst.playlist_id, is_template=False playlist_dst.playlist_id, is_template=False
) )
for row in range(self.ROWS_TO_CREATE): for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row(note=str(row)) model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)
@ -227,7 +282,13 @@ class TestMMMiscRowMove(unittest.TestCase):
playlist_dst.playlist_id, is_template=False playlist_dst.playlist_id, is_template=False
) )
for row in range(self.ROWS_TO_CREATE): for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row(note=str(row)) model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)
@ -255,7 +316,13 @@ class TestMMMiscRowMove(unittest.TestCase):
playlist_dst.playlist_id, is_template=False playlist_dst.playlist_id, is_template=False
) )
for row in range(self.ROWS_TO_CREATE): for row in range(self.ROWS_TO_CREATE):
model_dst.insert_row(note=str(row)) model_dst.insert_row_signal_handler(
InsertTrack(
playlist_id=playlist_dst.playlist_id,
track_id=None,
note=str(row)
)
)
model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, playlist_dst.playlist_id)

View File

@ -99,7 +99,7 @@ class MyTestCase(unittest.TestCase):
model = playlistmodel.PlaylistModel(playlist.playlist_id, is_template=False) model = playlistmodel.PlaylistModel(playlist.playlist_id, is_template=False)
# Add a track with a note # Add a track with a note
model.insert_row(track_id=self.track1.track_id, note=note_text) model.insert_row_signal_handler(track_id=self.track1.track_id, note=note_text)
# Retrieve playlist # Retrieve playlist
all_playlists = ds.playlists_all() all_playlists = ds.playlists_all()