Compare commits

..

13 Commits

Author SHA1 Message Date
Keith Edmunds
c9f774f2e4 WIP: Unify function to move rows within/between playlists 2025-03-28 09:57:36 +00:00
Keith Edmunds
d9a3dd0ec4 Remove kae.py from git 2025-03-28 07:47:35 +00:00
Keith Edmunds
d84cf91e9d WIP: Move rows within and between playlists working 2025-03-28 07:46:14 +00:00
Keith Edmunds
346509f6ca WIP: moving rows within playlist works 2025-03-27 11:24:13 +00:00
Keith Edmunds
4c1ee0b1ca WIP: all tests for move rows within playlist working 2025-03-22 20:54:04 +00:00
Keith Edmunds
bc7d6818aa WIP: move within playlist tests working 2025-03-22 18:53:14 +00:00
Keith Edmunds
0f8409879c Report correct line for ApplicationError 2025-03-22 09:27:55 +00:00
Keith Edmunds
a95aa918b1 WIP: Can play tracks without errors 2025-03-21 12:10:46 +00:00
Keith Edmunds
7361086da5 Use @singleton decorator 2025-03-20 17:44:33 +00:00
Keith Edmunds
e40a4ab57a WIP remove sessions, use reporistory 2025-03-17 18:43:46 +00:00
Keith Edmunds
e733e7025d WIP: playlists load, can't play track 2025-03-16 10:36:10 +00:00
Keith Edmunds
b520178e3a Keep track of selected rows in model 2025-03-14 13:22:12 +00:00
Keith Edmunds
9e07e73167 WIP: Use PlaylistRowDTO to isolate SQLAlchemy objects 2025-03-14 13:21:46 +00:00
5 changed files with 101 additions and 192 deletions

View File

@ -104,14 +104,16 @@ class FileImporter:
# variable or an instance variable are effectively the same thing. # variable or an instance variable are effectively the same thing.
workers: dict[str, DoTrackImport] = {} workers: dict[str, DoTrackImport] = {}
def __init__(self, base_model: PlaylistModel, row_number: int) -> None: def __init__(
self, base_model: PlaylistModel, row_number: Optional[int] = None
) -> None:
""" """
Initialise the FileImporter singleton instance. Initialise the FileImporter singleton instance.
""" """
log.debug(f"FileImporter.__init__({base_model=}, {row_number=})")
# Create ModelData # Create ModelData
if not row_number:
row_number = base_model.rowCount()
self.model_data = ThreadData(base_model=base_model, row_number=row_number) self.model_data = ThreadData(base_model=base_model, row_number=row_number)
# Data structure to track files to import # Data structure to track files to import

View File

@ -1,7 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# Standard library imports # Standard library imports
from collections import defaultdict from collections import defaultdict
from functools import wraps
import logging import logging
import logging.config import logging.config
import logging.handlers import logging.handlers
@ -121,30 +120,4 @@ def handle_exception(exc_type, exc_value, exc_traceback):
QMessageBox.critical(None, "Application Error", msg) QMessageBox.critical(None, "Application Error", msg)
def truncate_large(obj, limit=5):
"""Helper to truncate large lists or other iterables."""
if isinstance(obj, (list, tuple, set)):
if len(obj) > limit:
return f"{type(obj).__name__}(len={len(obj)}, items={list(obj)[:limit]}...)"
return repr(obj)
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
args_repr = [truncate_large(a) for a in args]
kwargs_repr = [f"{k}={truncate_large(v)}" for k, v in kwargs.items()]
params_repr = ", ".join(args_repr + kwargs_repr)
log.debug(f"call {func.__name__}({params_repr})", stacklevel=2)
try:
result = func(*args, **kwargs)
log.debug(f"return {func.__name__}: {truncate_large(result)}", stacklevel=2)
return result
except Exception as e:
log.debug(f"exception in {func.__name__}: {e}", stacklevel=2)
raise
return wrapper
sys.excepthook = handle_exception sys.excepthook = handle_exception

View File

@ -73,7 +73,7 @@ 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
from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks from models import db, Playdates, PlaylistRows, Playlists, Queries, Settings, Tracks
from playlistrow import PlaylistRow, TrackSequence from playlistrow import PlaylistRow, TrackSequence
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
@ -478,7 +478,6 @@ class ManageQueries(ItemlistManager):
self.populate_table(query_list) self.populate_table(query_list)
@log_call
def delete_item(self, query_id: int) -> None: def delete_item(self, query_id: int) -> None:
"""delete query""" """delete query"""
@ -491,6 +490,7 @@ class ManageQueries(ItemlistManager):
"Delete query", "Delete query",
f"Delete query '{query.name}': " "Are you sure?", f"Delete query '{query.name}': " "Are you sure?",
): ):
log.debug(f"manage_queries: delete {query=}")
self.session.delete(query) self.session.delete(query)
self.session.commit() self.session.commit()
@ -583,7 +583,6 @@ class ManageTemplates(ItemlistManager):
self.populate_table(template_list) self.populate_table(template_list)
@log_call
def delete_item(self, template_id: int) -> None: def delete_item(self, template_id: int) -> None:
"""delete template""" """delete template"""
@ -607,6 +606,7 @@ class ManageTemplates(ItemlistManager):
else: else:
self.musicmuster.playlist_section.tabPlaylist.removeTab(open_idx) self.musicmuster.playlist_section.tabPlaylist.removeTab(open_idx)
log.debug(f"manage_templates: delete {template=}")
self.session.delete(template) self.session.delete(template)
self.session.commit() self.session.commit()
@ -1235,6 +1235,7 @@ class Window(QMainWindow):
for playlist_id, idx in open_playlist_ids.items(): for playlist_id, idx in open_playlist_ids.items():
playlist = session.get(Playlists, playlist_id) playlist = session.get(Playlists, playlist_id)
if playlist: if playlist:
log.debug(f"Set {playlist=} tab to {idx=}")
playlist.tab = idx playlist.tab = idx
# Save window attributes # Save window attributes
@ -1452,7 +1453,6 @@ class Window(QMainWindow):
# # # # # # # # # # Playlist management functions # # # # # # # # # # # # # # # # # # # # Playlist management functions # # # # # # # # # #
@log_call
def _create_playlist( def _create_playlist(
self, session: Session, name: str, template_id: int self, session: Session, name: str, template_id: int
) -> Playlists: ) -> Playlists:
@ -1461,9 +1461,10 @@ class Window(QMainWindow):
if template_id > 0, and return the Playlists object. if template_id > 0, and return the Playlists object.
""" """
log.debug(f" _create_playlist({name=}, {template_id=})")
return Playlists(session, name, template_id) return Playlists(session, name, template_id)
@log_call
def _open_playlist(self, playlist: Playlists, is_template: bool = False) -> int: def _open_playlist(self, playlist: Playlists, is_template: bool = False) -> int:
""" """
With passed playlist: With passed playlist:
@ -1474,6 +1475,8 @@ class Window(QMainWindow):
return: tab index return: tab index
""" """
log.debug(f" _open_playlist({playlist=}, {is_template=})")
# Create base model and proxy model # Create base model and proxy model
base_model = PlaylistModel(playlist.id, is_template) base_model = PlaylistModel(playlist.id, is_template)
proxy_model = PlaylistProxyModel() proxy_model = PlaylistProxyModel()
@ -1492,7 +1495,6 @@ class Window(QMainWindow):
return idx return idx
@log_call
def create_playlist_from_template(self, session: Session, template_id: int) -> None: def create_playlist_from_template(self, session: Session, 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
@ -1514,8 +1516,7 @@ class Window(QMainWindow):
self._open_playlist(playlist) self._open_playlist(playlist)
session.commit() session.commit()
@log_call def delete_playlist(self) -> None:
def delete_playlist(self, checked: bool = False) -> None:
""" """
Delete current playlist Delete current playlist
""" """
@ -1534,7 +1535,7 @@ class Window(QMainWindow):
else: else:
log.error("Failed to retrieve playlist") log.error("Failed to retrieve playlist")
def open_existing_playlist(self, checked: bool = False) -> None: def open_existing_playlist(self) -> None:
"""Open existing playlist""" """Open existing playlist"""
with db.Session() as session: with db.Session() as session:
@ -1546,7 +1547,7 @@ class Window(QMainWindow):
self._open_playlist(playlist) self._open_playlist(playlist)
session.commit() session.commit()
def save_as_template(self, checked: bool = False) -> None: def save_as_template(self) -> None:
"""Save current playlist as template""" """Save current playlist as template"""
with db.Session() as session: with db.Session() as session:
@ -1622,7 +1623,7 @@ class Window(QMainWindow):
# # # # # # # # # # Manage templates and queries # # # # # # # # # # # # # # # # # # # # Manage templates and queries # # # # # # # # # #
def manage_queries_wrapper(self, checked: bool = False) -> None: def manage_queries_wrapper(self):
""" """
Simply instantiate the manage_queries class Simply instantiate the manage_queries class
""" """
@ -1630,7 +1631,7 @@ class Window(QMainWindow):
with db.Session() as session: with db.Session() as session:
_ = ManageQueries(session, self) _ = ManageQueries(session, self)
def manage_templates_wrapper(self, checked: bool = False) -> None: def manage_templates_wrapper(self):
""" """
Simply instantiate the manage_queries class Simply instantiate the manage_queries class
""" """
@ -1640,12 +1641,12 @@ class Window(QMainWindow):
# # # # # # # # # # Miscellaneous functions # # # # # # # # # # # # # # # # # # # # Miscellaneous functions # # # # # # # # # #
def select_duplicate_rows(self, checked: bool = False) -> None: def select_duplicate_rows(self) -> 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) -> None:
"""Get git tag and database name""" """Get git tag and database name"""
try: try:
@ -1674,7 +1675,7 @@ class Window(QMainWindow):
self.track_sequence.set_next(None) self.track_sequence.set_next(None)
self.update_headers() self.update_headers()
def clear_selection(self, checked: bool = False) -> None: def clear_selection(self) -> None:
"""Clear row selection""" """Clear row selection"""
# Unselect any selected rows # Unselect any selected rows
@ -1684,7 +1685,7 @@ class Window(QMainWindow):
# Clear the search bar # Clear the search bar
self.search_playlist_clear() self.search_playlist_clear()
def close_playlist_tab(self, checked: bool = False) -> bool: def close_playlist_tab(self) -> bool:
""" """
Close active playlist tab, called by menu item Close active playlist tab, called by menu item
""" """
@ -1770,7 +1771,6 @@ class Window(QMainWindow):
self.signals.search_songfacts_signal.connect(self.open_songfacts_browser) self.signals.search_songfacts_signal.connect(self.open_songfacts_browser)
self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser) self.signals.search_wikipedia_signal.connect(self.open_wikipedia_browser)
@log_call
def current_row_or_end(self) -> int: def current_row_or_end(self) -> int:
""" """
If a row or rows are selected, return the row number of the first If a row or rows are selected, return the row number of the first
@ -1782,14 +1782,14 @@ class Window(QMainWindow):
return self.current.selected_row_numbers[0] return self.current.selected_row_numbers[0]
return self.current.base_model.rowCount() return self.current.base_model.rowCount()
def debug(self, checked: bool = False) -> None: def debug(self):
"""Invoke debugger""" """Invoke debugger"""
import ipdb # type: ignore import ipdb # type: ignore
ipdb.set_trace() ipdb.set_trace()
def download_played_tracks(self, checked: bool = False) -> None: def download_played_tracks(self) -> None:
"""Download a CSV of played tracks""" """Download a CSV of played tracks"""
dlg = DownloadCSV(self) dlg = DownloadCSV(self)
@ -1820,7 +1820,6 @@ class Window(QMainWindow):
if self.track_sequence.current: if self.track_sequence.current:
self.track_sequence.current.drop3db(self.footer_section.btnDrop3db.isChecked()) self.track_sequence.current.drop3db(self.footer_section.btnDrop3db.isChecked())
@log_call
def enable_escape(self, enabled: bool) -> None: def enable_escape(self, enabled: bool) -> None:
""" """
Manage signal to enable/disable handling ESC character. Manage signal to enable/disable handling ESC character.
@ -1829,10 +1828,11 @@ class Window(QMainWindow):
so we need to disable it here while editing. so we need to disable it here while editing.
""" """
log.debug(f"enable_escape({enabled=})")
if "clear_selection" in self.menu_actions: if "clear_selection" in self.menu_actions:
self.menu_actions["clear_selection"].setEnabled(enabled) self.menu_actions["clear_selection"].setEnabled(enabled)
@log_call
def end_of_track_actions(self) -> None: def end_of_track_actions(self) -> None:
""" """
@ -1867,7 +1867,7 @@ class Window(QMainWindow):
# if not self.stop_autoplay: # if not self.stop_autoplay:
# self.play_next() # self.play_next()
def export_playlist_tab(self, checked: bool = False) -> None: def export_playlist_tab(self) -> None:
"""Export the current playlist to an m3u file""" """Export the current playlist to an m3u file"""
playlist_id = self.current.playlist_id playlist_id = self.current.playlist_id
@ -1908,7 +1908,7 @@ class Window(QMainWindow):
"\n" "\n"
) )
def fade(self, checked: bool = False) -> None: def fade(self) -> None:
"""Fade currently playing track""" """Fade currently playing track"""
if self.track_sequence.current: if self.track_sequence.current:
@ -1944,7 +1944,7 @@ class Window(QMainWindow):
# 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) -> None:
""" """
Pass import files call to file_importer module Pass import files call to file_importer module
""" """
@ -1954,7 +1954,7 @@ class Window(QMainWindow):
self.importer = FileImporter(self.current.base_model, self.current_row_or_end()) self.importer = FileImporter(self.current.base_model, self.current_row_or_end())
self.importer.start() self.importer.start()
def insert_header(self, checked: bool = False) -> None: def insert_header(self) -> None:
"""Show dialog box to enter header text and add to playlist""" """Show dialog box to enter header text and add to playlist"""
# Get header text # Get header text
@ -1969,7 +1969,7 @@ class Window(QMainWindow):
note=dlg.textValue(), note=dlg.textValue(),
) )
def insert_track(self, checked: bool = False) -> None: def insert_track(self) -> None:
"""Show dialog box to select and add track from database""" """Show dialog box to select and add track from database"""
dlg = TrackInsertDialog( dlg = TrackInsertDialog(
@ -1978,7 +1978,6 @@ class Window(QMainWindow):
) )
dlg.exec() dlg.exec()
@log_call
def load_last_playlists(self) -> None: def load_last_playlists(self) -> None:
"""Load the playlists that were open when the last session closed""" """Load the playlists that were open when the last session closed"""
@ -1986,6 +1985,7 @@ class Window(QMainWindow):
with db.Session() as session: with db.Session() as session:
for playlist in Playlists.get_open(session): for playlist in Playlists.get_open(session):
if playlist: if playlist:
log.debug(f"load_last_playlists() loaded {playlist=}")
# Create tab # Create tab
playlist_ids.append(self._open_playlist(playlist)) playlist_ids.append(self._open_playlist(playlist))
@ -2001,7 +2001,7 @@ class Window(QMainWindow):
Playlists.clear_tabs(session, playlist_ids) Playlists.clear_tabs(session, playlist_ids)
session.commit() session.commit()
def lookup_row_in_songfacts(self, checked: bool = False) -> None: def lookup_row_in_songfacts(self) -> None:
""" """
Display songfacts page for title in highlighted row Display songfacts page for title in highlighted row
""" """
@ -2012,7 +2012,7 @@ class Window(QMainWindow):
self.signals.search_songfacts_signal.emit(track_info.title) self.signals.search_songfacts_signal.emit(track_info.title)
def lookup_row_in_wikipedia(self, checked: bool = False) -> None: def lookup_row_in_wikipedia(self) -> None:
""" """
Display Wikipedia page for title in highlighted row or next track Display Wikipedia page for title in highlighted row or next track
""" """
@ -2023,7 +2023,7 @@ class Window(QMainWindow):
self.signals.search_wikipedia_signal.emit(track_info.title) self.signals.search_wikipedia_signal.emit(track_info.title)
def mark_rows_for_moving(self, checked: bool = False) -> None: def mark_rows_for_moving(self) -> None:
""" """
Cut rows ready for pasting. Cut rows ready for pasting.
""" """
@ -2037,7 +2037,6 @@ class Window(QMainWindow):
f"mark_rows_for_moving(): {self.move_source_rows=} {self.move_source_model=}" f"mark_rows_for_moving(): {self.move_source_rows=} {self.move_source_model=}"
) )
@log_call
def move_playlist_rows(self, row_numbers: list[int]) -> None: def move_playlist_rows(self, row_numbers: list[int]) -> None:
""" """
Move passed playlist rows to another playlist Move passed playlist rows to another playlist
@ -2075,7 +2074,7 @@ class Window(QMainWindow):
# Reset track_sequences # Reset track_sequences
self.track_sequence.update() self.track_sequence.update()
def move_selected(self, checked: bool = False) -> None: def move_selected(self) -> None:
""" """
Move selected rows to another playlist Move selected rows to another playlist
""" """
@ -2086,7 +2085,7 @@ class Window(QMainWindow):
self.move_playlist_rows(selected_rows) self.move_playlist_rows(selected_rows)
def move_unplayed(self, checked: bool = False) -> None: def move_unplayed(self) -> None:
""" """
Move unplayed rows to another playlist Move unplayed rows to another playlist
""" """
@ -2118,8 +2117,7 @@ class Window(QMainWindow):
webbrowser.get("browser").open_new_tab(url) webbrowser.get("browser").open_new_tab(url)
@log_call def paste_rows(self, dummy_for_profiling: int | None = None) -> None:
def paste_rows(self, checked: bool = False) -> None:
""" """
Paste earlier cut rows. Paste earlier cut rows.
""" """
@ -2152,8 +2150,7 @@ class Window(QMainWindow):
if set_next_row: if set_next_row:
to_playlist_model.set_next_row(set_next_row) to_playlist_model.set_next_row(set_next_row)
@log_call def play_next(self, position: Optional[float] = None) -> None:
def play_next(self, position: Optional[float] = None, checked: bool = False) -> None:
""" """
Play next track, optionally from passed position. Play next track, optionally from passed position.
@ -2348,7 +2345,7 @@ class Window(QMainWindow):
if ok: if ok:
log.debug("quicklog: " + dlg.textValue()) log.debug("quicklog: " + dlg.textValue())
def rename_playlist(self, checked: bool = False) -> None: def rename_playlist(self) -> None:
""" """
Rename current playlist Rename current playlist
""" """
@ -2404,7 +2401,7 @@ class Window(QMainWindow):
return False return False
def resume(self, checked: bool = False) -> None: def resume(self) -> None:
""" """
Resume playing last track. We may be playing the next track Resume playing last track. We may be playing the next track
or none; take care of both eventualities. or none; take care of both eventualities.
@ -2445,7 +2442,7 @@ class Window(QMainWindow):
) )
self.track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms) self.track_sequence.current.start_time -= dt.timedelta(milliseconds=elapsed_ms)
def search_playlist(self, checked: bool = False) -> None: def search_playlist(self) -> None:
"""Show text box to search playlist""" """Show text box to search playlist"""
# Disable play controls so that 'return' in search box doesn't # Disable play controls so that 'return' in search box doesn't
@ -2503,8 +2500,7 @@ class Window(QMainWindow):
height = Settings.get_setting(session, "mainwindow_height").f_int or 100 height = Settings.get_setting(session, "mainwindow_height").f_int or 100
self.setGeometry(x, y, width, height) self.setGeometry(x, y, width, height)
@log_call def set_selected_track_next(self) -> None:
def set_selected_track_next(self, checked: bool = False) -> None:
""" """
Set currently-selected row on visible playlist tab as next track Set currently-selected row on visible playlist tab as next track
""" """
@ -2517,7 +2513,6 @@ class Window(QMainWindow):
# else: # else:
# log.error("No active tab") # log.error("No active tab")
@log_call
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
""" """
Find the tab containing the widget and set the text colour Find the tab containing the widget and set the text colour
@ -2579,8 +2574,7 @@ class Window(QMainWindow):
self.active_tab().scroll_to_top(playlist_track.row_number) self.active_tab().scroll_to_top(playlist_track.row_number)
@log_call def stop(self) -> None:
def stop(self, checked: bool = False) -> None:
"""Stop playing immediately""" """Stop playing immediately"""
self.stop_autoplay = True self.stop_autoplay = True
@ -2714,7 +2708,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)
# Re-enable 10ms timer (see above) # Re-enable 10ms timer (see above)
log.debug(f"issue287: {self.timer10.isActive()=}")
if not self.timer10.isActive(): if not self.timer10.isActive():
self.timer10.start(10) self.timer10.start(10)
log.debug("issue223: update_clocks: 10ms timer enabled") log.debug("issue223: update_clocks: 10ms timer enabled")

View File

@ -46,7 +46,7 @@ from helpers import (
remove_substring_case_insensitive, remove_substring_case_insensitive,
set_track_metadata, set_track_metadata,
) )
from log import log, log_call from log import log
from models import db, NoteColours, Playdates, PlaylistRows, Tracks from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from playlistrow import PlaylistRow, TrackSequence from playlistrow import PlaylistRow, TrackSequence
import repository import repository
@ -79,6 +79,8 @@ class PlaylistModel(QAbstractTableModel):
) -> None: ) -> None:
super().__init__() super().__init__()
log.debug("PlaylistModel.__init__()")
self.playlist_id = playlist_id self.playlist_id = playlist_id
self.is_template = is_template self.is_template = is_template
self.track_sequence = TrackSequence() self.track_sequence = TrackSequence()
@ -143,7 +145,6 @@ class PlaylistModel(QAbstractTableModel):
return header_row return header_row
@log_call
def add_track_to_header( def add_track_to_header(
self, playlist_id: int, track_id: int, note: str = "" self, playlist_id: int, track_id: int, note: str = ""
) -> None: ) -> None:
@ -241,7 +242,6 @@ class PlaylistModel(QAbstractTableModel):
return QBrush() return QBrush()
@log_call
def begin_reset_model(self, playlist_id: int) -> None: def begin_reset_model(self, playlist_id: int) -> None:
""" """
Reset model if playlist_id is ours Reset model if playlist_id is ours
@ -256,7 +256,6 @@ class PlaylistModel(QAbstractTableModel):
return len(Col) return len(Col)
@log_call
def current_track_started(self) -> None: def current_track_started(self) -> None:
""" """
Notification from musicmuster that the current track has just Notification from musicmuster that the current track has just
@ -272,6 +271,8 @@ class PlaylistModel(QAbstractTableModel):
- update track times - update track times
""" """
log.debug(f"{self}: current_track_started()")
if not self.track_sequence.current: if not self.track_sequence.current:
return return
@ -379,7 +380,6 @@ class PlaylistModel(QAbstractTableModel):
return QVariant() return QVariant()
@log_call
def delete_rows(self, row_numbers: list[int]) -> None: def delete_rows(self, row_numbers: list[int]) -> None:
""" """
Delete passed rows from model Delete passed rows from model
@ -462,13 +462,15 @@ class PlaylistModel(QAbstractTableModel):
return "" return ""
@log_call
def end_reset_model(self, playlist_id: int) -> None: def end_reset_model(self, playlist_id: int) -> None:
""" """
End model reset if this is our playlist End model reset if this is our playlist
""" """
log.debug(f"{self}: end_reset_model({playlist_id=})")
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
log.debug(f"{self}: end_reset_model: not us ({self.playlist_id=})")
return return
with db.Session() as session: with db.Session() as session:
self.refresh_data(session) self.refresh_data(session)
@ -548,13 +550,14 @@ class PlaylistModel(QAbstractTableModel):
return boldfont return boldfont
@log_call
def get_duplicate_rows(self) -> list[int]: def get_duplicate_rows(self) -> list[int]:
""" """
Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4] Return a list of duplicate rows. If track appears in rows 2, 3 and 4, return [3, 4]
(ie, ignore the first, not-yet-duplicate, track). (ie, ignore the first, not-yet-duplicate, track).
""" """
log.debug(f"{self}: get_duplicate_rows() called")
found = [] found = []
result = [] result = []
@ -567,9 +570,9 @@ class PlaylistModel(QAbstractTableModel):
else: else:
found.append(track_id) found.append(track_id)
log.debug(f"{self}: get_duplicate_rows() returned: {result=}")
return result return result
@log_call
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int: def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
""" """
Sanitises proposed new row number. Sanitises proposed new row number.
@ -578,6 +581,8 @@ class PlaylistModel(QAbstractTableModel):
If not given, return row number to add to end of model. If not given, return row number to add to end of model.
""" """
log.debug(f"{self}: _get_new_row_number({proposed_row_number=})")
if proposed_row_number is None or proposed_row_number > len(self.playlist_rows): if proposed_row_number is None or proposed_row_number > len(self.playlist_rows):
# We are adding to the end of the list # We are adding to the end of the list
new_row_number = len(self.playlist_rows) new_row_number = len(self.playlist_rows)
@ -587,6 +592,7 @@ class PlaylistModel(QAbstractTableModel):
else: else:
new_row_number = proposed_row_number new_row_number = proposed_row_number
log.debug(f"{self}: get_new_row_number() return: {new_row_number=}")
return new_row_number return new_row_number
def get_row_info(self, row_number: int) -> PlaylistRow: def get_row_info(self, row_number: int) -> PlaylistRow:
@ -596,7 +602,6 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number] return self.playlist_rows[row_number]
@log_call
def get_row_track_id(self, row_number: int) -> Optional[int]: def get_row_track_id(self, row_number: int) -> Optional[int]:
""" """
Return id of track associated with row or None if no track associated Return id of track associated with row or None if no track associated
@ -713,7 +718,6 @@ class PlaylistModel(QAbstractTableModel):
] ]
self.invalidate_row(row_number, roles) self.invalidate_row(row_number, roles)
@log_call
def insert_row( def insert_row(
self, self,
proposed_row_number: Optional[int], proposed_row_number: Optional[int],
@ -724,6 +728,8 @@ class PlaylistModel(QAbstractTableModel):
Insert a row. Insert a row.
""" """
log.debug(f"{self}: insert_row({proposed_row_number=}, {track_id=}, {note=})")
new_row_number = self._get_new_row_number(proposed_row_number) new_row_number = self._get_new_row_number(proposed_row_number)
super().beginInsertRows(QModelIndex(), new_row_number, new_row_number) super().beginInsertRows(QModelIndex(), new_row_number, new_row_number)
@ -756,12 +762,13 @@ class PlaylistModel(QAbstractTableModel):
list(range(new_row_number, len(self.playlist_rows))), roles_to_invalidate list(range(new_row_number, len(self.playlist_rows))), roles_to_invalidate
) )
@log_call
def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None: def invalidate_row(self, modified_row: int, roles: list[Qt.ItemDataRole]) -> None:
""" """
Signal to view to refresh invalidated row Signal to view to refresh invalidated row
""" """
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
self.dataChanged.emit( self.dataChanged.emit(
self.index(modified_row, 0), self.index(modified_row, 0),
self.index(modified_row, self.columnCount() - 1), self.index(modified_row, self.columnCount() - 1),
@ -775,6 +782,8 @@ class PlaylistModel(QAbstractTableModel):
Signal to view to refresh invlidated rows Signal to view to refresh invlidated rows
""" """
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
for modified_row in modified_rows: for modified_row in modified_rows:
# only invalidate required roles # only invalidate required roles
self.invalidate_row(modified_row, roles) self.invalidate_row(modified_row, roles)
@ -788,7 +797,6 @@ class PlaylistModel(QAbstractTableModel):
return self.playlist_rows[row_number].path == "" return self.playlist_rows[row_number].path == ""
return False return False
@log_call
def is_played_row(self, row_number: int) -> bool: def is_played_row(self, row_number: int) -> bool:
""" """
Return True if row is an unplayed track row, else False Return True if row is an unplayed track row, else False
@ -824,7 +832,6 @@ class PlaylistModel(QAbstractTableModel):
] ]
self.invalidate_rows(row_numbers, roles) self.invalidate_rows(row_numbers, roles)
@log_call
def move_rows(self, from_rows: list[int], to_row_number: int) -> bool: def move_rows(self, from_rows: list[int], to_row_number: int) -> bool:
""" """
Move the playlist rows in from_rows to to_row. Return True if successful Move the playlist rows in from_rows to to_row. Return True if successful
@ -846,46 +853,11 @@ class PlaylistModel(QAbstractTableModel):
) )
from_rows.remove(self.track_sequence.current.row_number) from_rows.remove(self.track_sequence.current.row_number)
from_rows = sorted(set(from_rows)) # Row moves must be wrapped in beginMoveRows .. endMoveRows and
if (min(from_rows) < 0 or max(from_rows) >= self.rowCount() # the row range must be contiguous. Process the highest rows
or to_row_number < 0 or to_row_number > self.rowCount()): # first so the lower row numbers are unchanged
log.debug("move_rows: invalid indexes")
return False
if to_row_number in from_rows: row_groups = self._reversed_contiguous_row_groups([a.row_number for a in from_rows])
return False # Destination within rows to be moved
# # Remove rows from bottom to top to avoid index shifting
# for row in sorted(from_rows, reverse=True):
# self.beginRemoveRows(QModelIndex(), row, row)
# del self.playlist_rows[row]
# # At this point self.playlist_rows has been updated but the
# # underlying database has not (that's done below after
# # inserting the rows)
# self.endRemoveRows()
# # Adjust insertion point after removal
# if to_row_number > max(from_rows):
# rows_below_dest = len([r for r in from_rows if r < to_row_number])
# insertion_point = to_row_number - rows_below_dest
# else:
# insertion_point = to_row_number
# # Insert rows at the destination
# plrid_to_new_row_number: list[dict[int, int]] = []
# for offset, row_data in enumerate(rows_to_move):
# row_number = insertion_point + offset
# self.beginInsertRows(QModelIndex(), row_number, row_number)
# self.playlist_rows[row_number] = row_data
# plrid_to_new_row_number.append({row_data.playlistrow_id: row_number})
# self.endInsertRows()
# Notify model going to change
self.beginResetModel()
# Update database
repository.move_rows(from_rows, self.playlist_id, to_row_number)
# Notify model changed
self.endResetModel()
# Handle the moves in row_group chunks # Handle the moves in row_group chunks
for row_group in row_groups: for row_group in row_groups:
@ -937,7 +909,6 @@ class PlaylistModel(QAbstractTableModel):
super().endInsertRows() super().endInsertRows()
@log_call
def move_rows_between_playlists( def move_rows_between_playlists(
self, self,
from_rows: list[PlaylistRow], from_rows: list[PlaylistRow],
@ -948,6 +919,11 @@ class PlaylistModel(QAbstractTableModel):
Move the playlist rows given to to_row and below of to_playlist. Move the playlist rows given to to_row and below of to_playlist.
""" """
log.debug(
f"{self}: move_rows_between_playlists({from_rows=}, "
f"{to_row_number=}, {to_playlist_id=}"
)
# Don't move current row # Don't move current row
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
@ -986,7 +962,6 @@ class PlaylistModel(QAbstractTableModel):
self.track_sequence.update() self.track_sequence.update()
self.update_track_times() self.update_track_times()
@log_call
def move_track_add_note( def move_track_add_note(
self, new_row_number: int, existing_plr: PlaylistRow, note: str self, new_row_number: int, existing_plr: PlaylistRow, note: str
) -> None: ) -> None:
@ -994,6 +969,10 @@ class PlaylistModel(QAbstractTableModel):
Move existing_rat track to new_row_number and append note to any existing note Move existing_rat track to new_row_number and append note to any existing note
""" """
log.debug(
f"{self}: move_track_add_note({new_row_number=}, {existing_plr=}, {note=}"
)
if note: if note:
playlist_row = self.playlist_rows[existing_plr.row_number] playlist_row = self.playlist_rows[existing_plr.row_number]
if playlist_row.note: if playlist_row.note:
@ -1007,30 +986,14 @@ class PlaylistModel(QAbstractTableModel):
self.move_rows([existing_plr.row_number], new_row_number) self.move_rows([existing_plr.row_number], new_row_number)
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
@log_call
def move_track_to_header(
self,
header_row_number: int,
existing_rat: RowAndTrack,
note: Optional[str],
) -> None:
"""
Add the existing_rat track details to the existing header at header_row_number
"""
if existing_rat.track_id:
if note and existing_rat.note:
note += "\n" + existing_rat.note
self.add_track_to_header(header_row_number, existing_rat.track_id, note)
self.delete_rows([existing_rat.row_number])
@log_call
def obs_scene_change(self, row_number: int) -> None: def obs_scene_change(self, row_number: int) -> None:
""" """
Check this row and any preceding headers for OBS scene change command Check this row and any preceding headers for OBS scene change command
and execute any found and execute any found
""" """
log.debug(f"{self}: obs_scene_change({row_number=})")
# Check any headers before this row # Check any headers before this row
idx = row_number - 1 idx = row_number - 1
while self.is_header_row(idx): while self.is_header_row(idx):
@ -1059,7 +1022,6 @@ class PlaylistModel(QAbstractTableModel):
log.warning(f"{self}: OBS connection refused") log.warning(f"{self}: OBS connection refused")
return return
@log_call
def previous_track_ended(self) -> None: def previous_track_ended(self) -> None:
""" """
Notification from musicmuster that the previous track has ended. Notification from musicmuster that the previous track has ended.
@ -1069,6 +1031,8 @@ class PlaylistModel(QAbstractTableModel):
- update display - update display
""" """
log.debug(f"{self}: previous_track_ended()")
# Sanity check # Sanity check
if not self.track_sequence.previous: if not self.track_sequence.previous:
log.error( log.error(
@ -1124,12 +1088,13 @@ class PlaylistModel(QAbstractTableModel):
self.playlist_rows[row_number] = PlaylistRow(refreshed_row) self.playlist_rows[row_number] = PlaylistRow(refreshed_row)
@log_call
def remove_track(self, row_number: int) -> None: def remove_track(self, row_number: int) -> None:
""" """
Remove track from row, retaining row as a header row Remove track from row, retaining row as a header row
""" """
log.debug(f"{self}: remove_track({row_number=})")
self.playlist_rows[row_number].track_id = None self.playlist_rows[row_number].track_id = None
# only invalidate required roles # only invalidate required roles
@ -1159,31 +1124,6 @@ class PlaylistModel(QAbstractTableModel):
self.signals.resize_rows_signal.emit(self.playlist_id) self.signals.resize_rows_signal.emit(self.playlist_id)
session.commit() session.commit()
@log_call
def reset_track_sequence_row_numbers(self) -> None:
"""
Signal handler for when row ordering has changed.
Example: row 4 is marked as next. Row 2 is deleted. The PlaylistRows table will
be correctly updated with change of row number, but track_sequence.next will still
contain row_number==4. This function fixes up the track_sequence row numbers by
looking up the playlistrow_id and retrieving the row number from the database.
"""
# Check the track_sequence.next, current and previous plrs and
# update the row number
with db.Session() as session:
for ts in [
track_sequence.next,
track_sequence.current,
track_sequence.previous,
]:
if ts:
ts.update_playlist_and_row(session)
session.commit()
self.update_track_times()
def remove_comments(self, row_numbers: list[int]) -> None: def remove_comments(self, row_numbers: list[int]) -> None:
""" """
Remove comments from passed rows Remove comments from passed rows
@ -1222,10 +1162,7 @@ class PlaylistModel(QAbstractTableModel):
] ]
self.invalidate_rows(row_numbers, roles) self.invalidate_rows(row_numbers, roles)
@log_call def _reversed_contiguous_row_groups(self, row_numbers: list[int]) -> list[list[int]]:
def _reversed_contiguous_row_groups(
self, row_numbers: list[int]
) -> list[list[int]]:
""" """
Take the list of row numbers and split into groups of contiguous rows. Return as a list Take the list of row numbers and split into groups of contiguous rows. Return as a list
of lists with the highest row numbers first. of lists with the highest row numbers first.
@ -1235,6 +1172,8 @@ class PlaylistModel(QAbstractTableModel):
return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]] return: [[20, 21], [17], [13], [9, 10], [7], [2, 3, 4, 5]]
""" """
log.debug(f"{self}: _reversed_contiguous_row_groups({row_numbers=} called")
result: list[list[int]] = [] result: list[list[int]] = []
temp: list[int] = [] temp: list[int] = []
last_value = row_numbers[0] - 1 last_value = row_numbers[0] - 1
@ -1250,11 +1189,12 @@ class PlaylistModel(QAbstractTableModel):
result.append(temp) result.append(temp)
result.reverse() result.reverse()
log.debug(f"{self}: _reversed_contiguous_row_groups() returned: {result=}")
return result return result
def remove_section_timer_markers(self, header_text: str) -> str: def remove_section_timer_markers(self, header_text: str) -> str:
""" """
Remove characters used to mark section timings from Remove characters used to mark section timeings from
passed header text. passed header text.
Remove text using to signal header colours if colour entry Remove text using to signal header colours if colour entry
@ -1387,7 +1327,6 @@ class PlaylistModel(QAbstractTableModel):
return True return True
@log_call
def set_selected_rows(self, playlist_id: int, selected_row_numbers: list[int]) -> None: def set_selected_rows(self, playlist_id: int, selected_row_numbers: list[int]) -> None:
""" """
Handle signal_playlist_selected_rows to keep track of which rows Handle signal_playlist_selected_rows to keep track of which rows
@ -1607,7 +1546,6 @@ class PlaylistModel(QAbstractTableModel):
] ]
) )
@log_call
def update_or_insert(self, track_id: int, row_number: int) -> None: def update_or_insert(self, track_id: int, row_number: int) -> None:
""" """
If the passed track_id exists in this playlist, update the If the passed track_id exists in this playlist, update the
@ -1631,12 +1569,13 @@ class PlaylistModel(QAbstractTableModel):
else: else:
self.insert_row(proposed_row_number=row_number, track_id=track_id) self.insert_row(proposed_row_number=row_number, track_id=track_id)
@log_call
def update_track_times(self) -> None: def update_track_times(self) -> None:
""" """
Update track start/end times in self.playlist_rows Update track start/end times in self.playlist_rows
""" """
log.debug(f"issue285: {self}: update_track_times()")
next_start_time: Optional[dt.datetime] = None next_start_time: Optional[dt.datetime] = None
update_rows: list[int] = [] update_rows: list[int] = []
row_count = len(self.playlist_rows) row_count = len(self.playlist_rows)

View File

@ -44,7 +44,7 @@ from helpers import (
show_OK, show_OK,
show_warning, show_warning,
) )
from log import log, log_call from log import log
from models import db, Settings from models import db, Settings
from playlistrow import TrackSequence from playlistrow import TrackSequence
from playlistmodel import PlaylistModel, PlaylistProxyModel from playlistmodel import PlaylistModel, PlaylistProxyModel
@ -359,8 +359,7 @@ class PlaylistTab(QTableView):
# Deselect edited line # Deselect edited line
self.clear_selection() self.clear_selection()
@log_call def dropEvent(self, event: Optional[QDropEvent], dummy: int | None = None) -> None:
def dropEvent(self, event: Optional[QDropEvent]) -> None:
""" """
Move dropped rows Move dropped rows
""" """
@ -396,6 +395,9 @@ class PlaylistTab(QTableView):
destination_index = to_index destination_index = to_index
to_model_row = self.model().mapToSource(destination_index).row() to_model_row = self.model().mapToSource(destination_index).row()
log.debug(
f"PlaylistTab.dropEvent(): {from_rows=}, {destination_index=}, {to_model_row=}"
)
# Sanity check # Sanity check
base_model_row_count = self.get_base_model().rowCount() base_model_row_count = self.get_base_model().rowCount()
@ -678,6 +680,8 @@ class PlaylistTab(QTableView):
Called when column width changes. Save new width to database. Called when column width changes. Save new width to database.
""" """
log.debug(f"_column_resize({column_number=}, {_old=}, {_new=}")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:
return return
@ -722,7 +726,6 @@ class PlaylistTab(QTableView):
cb.clear(mode=cb.Mode.Clipboard) cb.clear(mode=cb.Mode.Clipboard)
cb.setText(track_path, mode=cb.Mode.Clipboard) cb.setText(track_path, mode=cb.Mode.Clipboard)
@log_call
def current_track_started(self) -> None: def current_track_started(self) -> None:
""" """
Called when track starts playing Called when track starts playing
@ -810,7 +813,6 @@ class PlaylistTab(QTableView):
else: else:
return TrackInfo(track_id, selected_row) return TrackInfo(track_id, selected_row)
@log_call
def get_selected_row(self) -> Optional[int]: def get_selected_row(self) -> Optional[int]:
""" """
Return selected row number. If no rows or multiple rows selected, return None Return selected row number. If no rows or multiple rows selected, return None
@ -822,7 +824,6 @@ class PlaylistTab(QTableView):
else: else:
return None return None
@log_call
def get_selected_rows(self) -> list[int]: def get_selected_rows(self) -> list[int]:
"""Return a list of model-selected row numbers sorted by row""" """Return a list of model-selected row numbers sorted by row"""
@ -835,7 +836,6 @@ class PlaylistTab(QTableView):
return sorted(list(set([self.model().mapToSource(a).row() for a in selected_indexes]))) return sorted(list(set([self.model().mapToSource(a).row() for a in selected_indexes])))
@log_call
def get_top_visible_row(self) -> int: def get_top_visible_row(self) -> int:
""" """
Get the viewport of the table view Get the viewport of the table view
@ -958,6 +958,8 @@ class PlaylistTab(QTableView):
If playlist_id is us, resize rows If playlist_id is us, resize rows
""" """
log.debug(f"resize_rows({playlist_id=}) {self.playlist_id=}")
if playlist_id and playlist_id != self.playlist_id: if playlist_id and playlist_id != self.playlist_id:
return return
@ -1004,7 +1006,6 @@ class PlaylistTab(QTableView):
# Reset selection mode # Reset selection mode
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
@log_call
def source_model_selected_row_number(self) -> Optional[int]: def source_model_selected_row_number(self) -> Optional[int]:
""" """
Return the model row number corresponding to the selected row or None Return the model row number corresponding to the selected row or None
@ -1015,7 +1016,6 @@ class PlaylistTab(QTableView):
return None return None
return self.model().mapToSource(selected_index).row() return self.model().mapToSource(selected_index).row()
@log_call
def selected_model_row_numbers(self) -> list[int]: def selected_model_row_numbers(self) -> list[int]:
""" """
Return a list of model row numbers corresponding to the selected rows or Return a list of model row numbers corresponding to the selected rows or
@ -1058,6 +1058,8 @@ class PlaylistTab(QTableView):
def _set_column_widths(self) -> None: def _set_column_widths(self) -> None:
"""Column widths from settings""" """Column widths from settings"""
log.debug("_set_column_widths()")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:
return return