Compare commits

...

13 Commits

Author SHA1 Message Date
Keith Edmunds
e43c9f3b17 Add successive tracks below those just added 2023-12-17 14:33:24 +00:00
Keith Edmunds
2bf1e442be Fix row spanning error leading to high CPU idle load 2023-12-17 00:12:39 +00:00
Keith Edmunds
4b6c8b0634 Rewrite logging
Add lots of log.info() statements
2023-12-17 00:12:03 +00:00
Keith Edmunds
2432039b72 Best-efforts resize row heights on open 2023-12-16 12:37:41 +00:00
Keith Edmunds
74bdbe2975 Improve open in / import from Audacity 2023-12-16 12:34:23 +00:00
Keith Edmunds
f228a371f2 Ensure all rows correctly resized for height 2023-12-16 02:37:49 +00:00
Keith Edmunds
b74007119d Name source and proxy models consistently 2023-12-16 02:36:16 +00:00
Keith Edmunds
45243759b8 Stackprinter dump if no fade graph 2023-12-15 18:46:30 +00:00
Keith Edmunds
d73bdb264d Don't prompt user when editing if no changes made 2023-12-15 18:46:03 +00:00
Keith Edmunds
90f8e20843 Fix scroll to current/next track with played tracks hidden 2023-12-15 18:27:42 +00:00
Keith Edmunds
184318078f Better fix for setting track/header row spans correctly 2023-12-15 17:55:22 +00:00
Keith Edmunds
c6befd219c Improve playlist load speed 2023-12-15 17:48:42 +00:00
Keith Edmunds
2f0ad5cd52 Fix track rows sometimes displayed as header rows 2023-12-15 14:10:54 +00:00
11 changed files with 414 additions and 252 deletions

View File

@ -58,8 +58,8 @@ class Config(object):
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"
LOG_LEVEL_STDERR = logging.ERROR LOG_LEVEL_STDERR = logging.INFO
LOG_LEVEL_SYSLOG = logging.DEBUG LOG_LEVEL_SYSLOG = logging.INFO
LOG_NAME = "musicmuster" LOG_NAME = "musicmuster"
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD")
MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25) MAIL_PORT = int(os.environ.get("MAIL_PORT") or 25)

View File

@ -10,6 +10,7 @@ from helpers import (
get_relative_date, get_relative_date,
ms_to_mmss, ms_to_mmss,
) )
from log import log
from models import Settings, Tracks from models import Settings, Tracks
from playlistmodel import PlaylistModel from playlistmodel import PlaylistModel
from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore from ui.dlg_TrackSelect_ui import Ui_Dialog # type: ignore
@ -22,7 +23,7 @@ class TrackSelectDialog(QDialog):
self, self,
session: scoped_session, session: scoped_session,
new_row_number: int, new_row_number: int,
model: PlaylistModel, source_model: PlaylistModel,
add_to_header: Optional[bool] = False, add_to_header: Optional[bool] = False,
*args, *args,
**kwargs, **kwargs,
@ -34,7 +35,7 @@ class TrackSelectDialog(QDialog):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.session = session self.session = session
self.new_row_number = new_row_number self.new_row_number = new_row_number
self.model = model self.source_model = source_model
self.add_to_header = add_to_header self.add_to_header = add_to_header
self.ui = Ui_Dialog() self.ui = Ui_Dialog()
self.ui.setupUi(self) self.ui.setupUi(self)
@ -69,25 +70,30 @@ class TrackSelectDialog(QDialog):
track = item.data(Qt.ItemDataRole.UserRole) track = item.data(Qt.ItemDataRole.UserRole)
note = self.ui.txtNote.text() note = self.ui.txtNote.text()
if not (track or note):
return
track_id = None track_id = None
if track: if track:
track_id = track.id track_id = track.id
if not track_id: if note and not track_id:
if note: self.source_model.insert_row(self.new_row_number, track_id, note)
self.model.insert_row(self.new_row_number, track_id, note)
self.ui.txtNote.clear() self.ui.txtNote.clear()
return self.new_row_number += 1
else:
# No note, no track
return return
self.ui.txtNote.clear() self.ui.txtNote.clear()
self.select_searchtext() self.select_searchtext()
if track_id is None:
log.error("track_id is None and should not be")
return
# Check whether track is already in playlist # Check whether track is already in playlist
move_existing = False move_existing = False
existing_prd = self.model.is_track_in_playlist(track_id) existing_prd = self.source_model.is_track_in_playlist(track_id)
if existing_prd is not None: if existing_prd is not None:
if ask_yes_no( if ask_yes_no(
"Duplicate row", "Duplicate row",
@ -98,17 +104,19 @@ class TrackSelectDialog(QDialog):
if self.add_to_header: if self.add_to_header:
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
self.model.move_track_to_header(self.new_row_number, existing_prd, note) self.source_model.move_track_to_header(self.new_row_number, existing_prd, note)
else: else:
self.model.add_track_to_header(self.new_row_number, track_id) self.source_model.add_track_to_header(self.new_row_number, track_id)
# Close dialog - we can only add one track to a header # Close dialog - we can only add one track to a header
self.accept() self.accept()
else: else:
# Adding a new track row # Adding a new track row
if move_existing and existing_prd: # "and existing_prd" for mypy's benefit if move_existing and existing_prd: # "and existing_prd" for mypy's benefit
self.model.move_track_add_note(self.new_row_number, existing_prd, note) self.source_model.move_track_add_note(self.new_row_number, existing_prd, note)
else: else:
self.model.insert_row(self.new_row_number, track_id, note) self.source_model.insert_row(self.new_row_number, track_id, note)
self.new_row_number += 1
def add_selected_and_close(self) -> None: def add_selected_and_close(self) -> None:
"""Handle Add and Close button""" """Handle Add and Close button"""

View File

@ -9,6 +9,7 @@ from PyQt6.QtWidgets import QTabWidget
from config import Config from config import Config
from classes import MusicMusterSignals from classes import MusicMusterSignals
from log import log
class InfoTabs(QTabWidget): class InfoTabs(QTabWidget):
@ -37,6 +38,7 @@ class InfoTabs(QTabWidget):
"""Search Songfacts for title""" """Search Songfacts for title"""
slug = slugify(title, replacements=([["'", ""]])) slug = slugify(title, replacements=([["'", ""]]))
log.info(f"Songfacts Infotab for {title=}")
url = f"https://www.songfacts.com/search/songs/{slug}" url = f"https://www.songfacts.com/search/songs/{slug}"
self.open_tab(url, title) self.open_tab(url, title)
@ -45,6 +47,7 @@ class InfoTabs(QTabWidget):
"""Search Wikipedia for title""" """Search Wikipedia for title"""
str = urllib.parse.quote_plus(title) str = urllib.parse.quote_plus(title)
log.info(f"Wikipedia Infotab for {title=}")
url = f"https://www.wikipedia.org/w/index.php?search={str}" url = f"https://www.wikipedia.org/w/index.php?search={str}"
self.open_tab(url, title) self.open_tab(url, title)

View File

@ -1,5 +1,6 @@
#!/usr/bin/python3 #!/usr/bin/python3
import colorlog
import logging import logging
import logging.handlers import logging.handlers
import os import os
@ -22,50 +23,29 @@ class LevelTagFilter(logging.Filter):
return True return True
class DebugStdoutFilter(logging.Filter):
"""Filter debug messages sent to stdout"""
def filter(self, record: logging.LogRecord):
# Exceptions are logged at ERROR level
if record.levelno in [logging.DEBUG, logging.ERROR]:
return True
if record.module in Config.DEBUG_MODULES:
return True
if record.funcName in Config.DEBUG_FUNCTIONS:
return True
return False
log = logging.getLogger(Config.LOG_NAME) log = logging.getLogger(Config.LOG_NAME)
log.setLevel(logging.DEBUG) log.setLevel(logging.DEBUG)
local_filter = LevelTagFilter()
# stderr # stderr
stderr = logging.StreamHandler() stderr = colorlog.StreamHandler()
stderr.setLevel(Config.LOG_LEVEL_STDERR) stderr.setLevel(Config.LOG_LEVEL_STDERR)
stderr.addFilter(local_filter)
stderr_fmt = colorlog.ColoredFormatter(
"%(log_color)s[%(asctime)s] %(filename)s:%(lineno)s %(leveltag)s:%(message)s",
datefmt="%H:%M:%S"
)
stderr.setFormatter(stderr_fmt)
log.addHandler(stderr)
# syslog # syslog
syslog = logging.handlers.SysLogHandler(address="/dev/log") syslog = logging.handlers.SysLogHandler(address="/dev/log")
syslog.setLevel(Config.LOG_LEVEL_SYSLOG) syslog.setLevel(Config.LOG_LEVEL_SYSLOG)
# Filter
local_filter = LevelTagFilter()
debug_filter = DebugStdoutFilter()
syslog.addFilter(local_filter) syslog.addFilter(local_filter)
stderr.addFilter(local_filter)
stderr.addFilter(debug_filter)
stderr_fmt = logging.Formatter(
"[%(asctime)s] %(leveltag)s: %(message)s", datefmt="%H:%M:%S"
)
syslog_fmt = logging.Formatter( syslog_fmt = logging.Formatter(
"[%(name)s] %(module)s.%(funcName)s - %(leveltag)s: %(message)s" "[%(name)s] %(filename)s:%(lineno)s %(leveltag)s: %(message)s"
) )
stderr.setFormatter(stderr_fmt)
syslog.setFormatter(syslog_fmt) syslog.setFormatter(syslog_fmt)
log.addHandler(stderr)
log.addHandler(syslog) log.addHandler(syslog)

View File

@ -66,6 +66,8 @@ class Music:
to hold up the UI during the fade. to hold up the UI during the fade.
""" """
log.info("Music.stop()")
if not self.player: if not self.player:
return return
@ -96,6 +98,8 @@ class Music:
Log and return if path not found. Log and return if path not found.
""" """
log.info(f"Music.play({path=}, {position=}")
if file_is_unreadable(path): if file_is_unreadable(path):
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
return None return None
@ -125,6 +129,8 @@ class Music:
def stop(self) -> float: def stop(self) -> float:
"""Immediately stop playing""" """Immediately stop playing"""
log.info("Music.stop()")
if not self.player: if not self.player:
return 0.0 return 0.0

View File

@ -148,11 +148,11 @@ class ImportTrack(QObject):
import_finished = pyqtSignal() import_finished = pyqtSignal()
def __init__( def __init__(
self, filenames: List[str], model: PlaylistModel, row_number: Optional[int] self, filenames: List[str], source_model: PlaylistModel, row_number: Optional[int]
) -> None: ) -> None:
super().__init__() super().__init__()
self.filenames = filenames self.filenames = filenames
self.model = model self.source_model = source_model
self.next_row_number = row_number self.next_row_number = row_number
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
@ -179,7 +179,7 @@ class ImportTrack(QObject):
# previous additions in this loop. So, commit now to # previous additions in this loop. So, commit now to
# lock in what we've just done. # lock in what we've just done.
session.commit() session.commit()
self.model.insert_row(self.next_row_number, track.id, "") self.source_model.insert_row(self.next_row_number, track.id, "")
self.next_row_number += 1 self.next_row_number += 1
self.signals.status_message_signal.emit( self.signals.status_message_signal.emit(
f"{len(self.filenames)} tracks imported", 10000 f"{len(self.filenames)} tracks imported", 10000
@ -216,11 +216,11 @@ class Window(QMainWindow, Ui_MainWindow):
FadeCurve.GraphWidget = self.widgetFadeVolume FadeCurve.GraphWidget = self.widgetFadeVolume
self.active_tab = lambda: self.tabPlaylist.currentWidget() self.active_tab = lambda: self.tabPlaylist.currentWidget()
self.active_model = lambda: self.tabPlaylist.currentWidget().model() self.active_proxy_model = lambda: self.tabPlaylist.currentWidget().model()
self.move_source_rows: Optional[List[int]] = None self.move_source_rows: Optional[List[int]] = None
self.move_source_model: Optional[PlaylistProxyModel] = None self.move_source_model: Optional[PlaylistProxyModel] = None
self.audacity_file_path: Optional[str] = None
self.load_last_playlists()
if Config.CARTS_HIDE: if Config.CARTS_HIDE:
self.cartsWidget.hide() self.cartsWidget.hide()
self.frame_6.hide() self.frame_6.hide()
@ -233,6 +233,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer1000.start(1000) self.timer1000.start(1000)
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.connect_signals_slots() self.connect_signals_slots()
self.load_last_playlists()
def about(self) -> None: def about(self) -> None:
"""Get git tag and database name""" """Get git tag and database name"""
@ -541,6 +542,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnStop.clicked.connect(self.stop) self.btnStop.clicked.connect(self.stop)
self.hdrCurrentTrack.clicked.connect(self.show_current) self.hdrCurrentTrack.clicked.connect(self.show_current)
self.hdrNextTrack.clicked.connect(self.show_next) self.hdrNextTrack.clicked.connect(self.show_next)
self.tabPlaylist.currentChanged.connect(self.tab_change)
self.tabPlaylist.tabCloseRequested.connect(self.close_tab) self.tabPlaylist.tabCloseRequested.connect(self.close_tab)
self.tabBar = self.tabPlaylist.tabBar() self.tabBar = self.tabPlaylist.tabBar()
self.txtSearch.textChanged.connect(self.search_playlist_text_changed) self.txtSearch.textChanged.connect(self.search_playlist_text_changed)
@ -559,6 +561,8 @@ class Window(QMainWindow, Ui_MainWindow):
) -> Optional[Playlists]: ) -> Optional[Playlists]:
"""Create new playlist""" """Create new playlist"""
log.info(f"create_playlist({playlist_name=}")
playlist_name = self.solicit_playlist_name(session) playlist_name = self.solicit_playlist_name(session)
if not playlist_name: if not playlist_name:
return None return None
@ -567,6 +571,8 @@ class Window(QMainWindow, Ui_MainWindow):
if playlist: if playlist:
playlist.mark_open() playlist.mark_open()
return playlist return playlist
else:
log.error("Failed to create playlist")
return None return None
@ -584,15 +590,15 @@ class Window(QMainWindow, Ui_MainWindow):
add tab to display. Return index number of tab. add tab to display. Return index number of tab.
""" """
assert playlist.id log.info(f"create_playlist_tab({playlist=})")
playlist_tab = PlaylistTab( playlist_tab = PlaylistTab(
musicmuster=self, musicmuster=self,
playlist_id=playlist.id, playlist_id=playlist.id,
) )
idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name)
self.tabPlaylist.setCurrentIndex(idx)
log.info(f"create_playlist_tab() returned: {idx=}")
return idx return idx
def cut_rows(self) -> None: def cut_rows(self) -> None:
@ -603,7 +609,9 @@ class Window(QMainWindow, Ui_MainWindow):
# Save the selected PlaylistRows items ready for a later # Save the selected PlaylistRows items ready for a later
# paste # paste
self.move_source_rows = self.active_tab().get_selected_rows() self.move_source_rows = self.active_tab().get_selected_rows()
self.move_source_model = self.active_model() self.move_source_model = self.active_proxy_model()
log.info(f"cut_rows(): {self.move_source_rows=} {self.move_source_model=}")
def debug(self): def debug(self):
"""Invoke debugger""" """Invoke debugger"""
@ -629,6 +637,8 @@ class Window(QMainWindow, Ui_MainWindow):
): ):
if self.close_playlist_tab(): if self.close_playlist_tab():
playlist.delete(session) playlist.delete(session)
else:
log.error("Failed to retrieve playlist")
def disable_play_next_controls(self) -> None: def disable_play_next_controls(self) -> None:
""" """
@ -679,6 +689,8 @@ class Window(QMainWindow, Ui_MainWindow):
so we need to disable it here while editing. so we need to disable it here while editing.
""" """
log.info(f"enable_escape({enabled=})")
self.action_Clear_selection.setEnabled(enabled) self.action_Clear_selection.setEnabled(enabled)
def enable_play_next_controls(self) -> None: def enable_play_next_controls(self) -> None:
@ -758,11 +770,11 @@ class Window(QMainWindow, Ui_MainWindow):
if self.hide_played_tracks: if self.hide_played_tracks:
self.hide_played_tracks = False self.hide_played_tracks = False
self.active_model().hide_played_tracks(False) self.active_proxy_model().hide_played_tracks(False)
self.btnHidePlayed.setText("Hide played") self.btnHidePlayed.setText("Hide played")
else: else:
self.hide_played_tracks = True self.hide_played_tracks = True
self.active_model().hide_played_tracks(True) self.active_proxy_model().hide_played_tracks(True)
self.btnHidePlayed.setText("Show played") self.btnHidePlayed.setText("Show played")
def import_track(self) -> None: def import_track(self) -> None:
@ -828,8 +840,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.import_thread = QThread() self.import_thread = QThread()
self.worker = ImportTrack( self.worker = ImportTrack(
new_tracks, new_tracks,
self.active_model(), self.active_proxy_model(),
self.active_tab().selected_model_row_number(), self.active_tab().source_model_selected_row_number(),
) )
self.worker.moveToThread(self.import_thread) self.worker.moveToThread(self.import_thread)
self.import_thread.started.connect(self.worker.run) self.import_thread.started.connect(self.worker.run)
@ -841,12 +853,9 @@ class Window(QMainWindow, Ui_MainWindow):
def insert_header(self) -> 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"""
try: proxy_model = self.active_proxy_model()
model = cast(PlaylistModel, self.active_tab().model()) if proxy_model is None:
if model is None: log.error("No proxy model")
return
except AttributeError:
# Just return if there's no visible playlist tab model
return return
# Get header text # Get header text
@ -856,8 +865,8 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100) dlg.resize(500, 100)
ok = dlg.exec() ok = dlg.exec()
if ok: if ok:
model.insert_row( proxy_model.insert_row(
proposed_row_number=self.active_tab().selected_model_row_number(), proposed_row_number=self.active_tab().source_model_selected_row_number(),
note=dlg.textValue(), note=dlg.textValue(),
) )
@ -867,8 +876,8 @@ class Window(QMainWindow, Ui_MainWindow):
with Session() as session: with Session() as session:
dlg = TrackSelectDialog( dlg = TrackSelectDialog(
session=session, session=session,
new_row_number=self.active_tab().selected_model_row_number(), new_row_number=self.active_tab().source_model_selected_row_number(),
model=self.active_model(), source_model=self.active_proxy_model(),
) )
dlg.exec() dlg.exec()
@ -881,6 +890,7 @@ class Window(QMainWindow, Ui_MainWindow):
if playlist: if playlist:
_ = self.create_playlist_tab(playlist) _ = self.create_playlist_tab(playlist)
playlist_ids.append(playlist.id) playlist_ids.append(playlist.id)
log.info(f"load_last_playlists() loaded {playlist=}")
# Set active tab # Set active tab
record = Settings.get_int_settings(session, "active_tab") record = Settings.get_int_settings(session, "active_tab")
if record.f_int is not None and record.f_int >= 0: if record.f_int is not None and record.f_int >= 0:
@ -897,11 +907,11 @@ class Window(QMainWindow, Ui_MainWindow):
Display songfacts page for title in highlighted row Display songfacts page for title in highlighted row
""" """
row_number = self.active_tab().selected_model_row_number() row_number = self.active_tab().source_model_selected_row_number()
if row_number is None: if row_number is None:
return return
track_info = self.active_model().get_row_info(row_number) track_info = self.active_proxy_model().get_row_info(row_number)
if track_info is None: if track_info is None:
return return
@ -912,11 +922,11 @@ class Window(QMainWindow, Ui_MainWindow):
Display Wikipedia page for title in highlighted row Display Wikipedia page for title in highlighted row
""" """
row_number = self.active_tab().selected_model_row_number() row_number = self.active_tab().source_model_selected_row_number()
if row_number is None: if row_number is None:
return return
track_info = self.active_model().get_row_info(row_number) track_info = self.active_proxy_model().get_row_info(row_number)
if track_info is None: if track_info is None:
return return
@ -953,7 +963,7 @@ class Window(QMainWindow, Ui_MainWindow):
to_row = 0 to_row = 0
# Move rows # Move rows
self.active_model().move_rows_between_playlists( self.active_proxy_model().move_rows_between_playlists(
row_numbers, to_row, to_playlist_id row_numbers, to_row, to_playlist_id
) )
@ -973,7 +983,7 @@ class Window(QMainWindow, Ui_MainWindow):
Move unplayed rows to another playlist Move unplayed rows to another playlist
""" """
unplayed_rows = self.active_model().get_unplayed_rows() unplayed_rows = self.active_proxy_model().get_unplayed_rows()
if not unplayed_rows: if not unplayed_rows:
return return
self.move_playlist_rows(unplayed_rows) self.move_playlist_rows(unplayed_rows)
@ -989,6 +999,7 @@ class Window(QMainWindow, Ui_MainWindow):
if template: if template:
playlist_name = self.solicit_playlist_name(session) playlist_name = self.solicit_playlist_name(session)
if not playlist_name: if not playlist_name:
log.error("Template has no name")
return return
playlist = Playlists.create_playlist_from_template( playlist = Playlists.create_playlist_from_template(
session, template, playlist_name session, template, playlist_name
@ -999,6 +1010,7 @@ class Window(QMainWindow, Ui_MainWindow):
session.commit() session.commit()
if playlist: if playlist:
log.error("Playlist failed to create")
playlist.mark_open() playlist.mark_open()
self.create_playlist_tab(playlist) self.create_playlist_tab(playlist)
@ -1011,9 +1023,11 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.exec() dlg.exec()
playlist = dlg.playlist playlist = dlg.playlist
if playlist: if playlist:
self.create_playlist_tab(playlist) idx = self.create_playlist_tab(playlist)
playlist.mark_open() playlist.mark_open()
self.tabPlaylist.setCurrentIndex(idx)
def paste_rows(self) -> None: def paste_rows(self) -> None:
""" """
Paste earlier cut rows. Paste earlier cut rows.
@ -1022,18 +1036,21 @@ class Window(QMainWindow, Ui_MainWindow):
if self.move_source_rows is None or self.move_source_model is None: if self.move_source_rows is None or self.move_source_model is None:
return return
to_playlist_id = self.active_tab().playlist_id to_playlist_model = self.active_tab().source_model
selected_rows = self.active_tab().get_selected_rows() selected_rows = self.active_tab().get_selected_rows()
if selected_rows: if selected_rows:
destination_row = selected_rows[0] destination_row = selected_rows[0]
else: else:
destination_row = self.active_model().rowCount() destination_row = self.active_proxy_model().rowCount()
if to_playlist_id == self.move_source_model.data_model.playlist_id: if (
to_playlist_model.playlist_id
== self.move_source_model.source_model.playlist_id
):
self.move_source_model.move_rows(self.move_source_rows, destination_row) self.move_source_model.move_rows(self.move_source_rows, destination_row)
else: else:
self.move_source_model.move_rows_between_playlists( self.move_source_model.move_rows_between_playlists(
self.move_source_rows, destination_row, to_playlist_id self.move_source_rows, destination_row, to_playlist_model
) )
self.move_source_rows = self.move_source_model = None self.move_source_rows = self.move_source_model = None
@ -1056,12 +1073,14 @@ class Window(QMainWindow, Ui_MainWindow):
- Update headers - Update headers
""" """
log.info(f"play_next({position=})")
# If there is no next track set, return. # If there is no next track set, return.
if not track_sequence.next.track_id: if not track_sequence.next.track_id:
log.debug("musicmuster.play_next(): no next track selected") log.error("musicmuster.play_next(): no next track selected")
return return
if not track_sequence.next.path: if not track_sequence.next.path:
log.debug("musicmuster.play_next(): no path for next track") log.error("musicmuster.play_next(): no path for next track")
return return
# If there's currently a track playing, fade it. # If there's currently a track playing, fade it.
@ -1082,9 +1101,12 @@ class Window(QMainWindow, Ui_MainWindow):
# Show closing volume graph # Show closing volume graph
if track_sequence.now.fade_graph: if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.plot() track_sequence.now.fade_graph.plot()
else:
log.error("No fade_graph")
# Play (new) current track # Play (new) current track
if not track_sequence.now.path: if not track_sequence.now.path:
log.error("No path for next track")
return return
track_sequence.now.start() track_sequence.now.start()
self.music.play(track_sequence.now.path, position) self.music.play(track_sequence.now.path, position)
@ -1099,12 +1121,12 @@ class Window(QMainWindow, Ui_MainWindow):
volume = self.music.player.audio_get_volume() volume = self.music.player.audio_get_volume()
if volume < Config.VOLUME_VLC_DEFAULT: if volume < Config.VOLUME_VLC_DEFAULT:
self.music.set_volume() self.music.set_volume()
log.error(f"Reset from {volume=}") log.warn(f"Reset from {volume=}")
break break
sleep(0.1) sleep(0.1)
# Notify model # Notify model
self.active_model().current_track_started() self.active_proxy_model().current_track_started()
# Note that track is now playing # Note that track is now playing
self.playing = True self.playing = True
@ -1163,8 +1185,11 @@ class Window(QMainWindow, Ui_MainWindow):
- If a track is playing, make that the next track - If a track is playing, make that the next track
""" """
log.info("resume()")
# Return if no saved position # Return if no saved position
if not track_sequence.previous.resume_marker: if not track_sequence.previous.resume_marker:
log.error("No previous track position")
return return
# We want to use play_next() to resume, so copy the previous # We want to use play_next() to resume, so copy the previous
@ -1235,7 +1260,7 @@ class Window(QMainWindow, Ui_MainWindow):
Incremental search of playlist Incremental search of playlist
""" """
self.active_model().set_incremental_search(self.txtSearch.text()) self.active_proxy_model().set_incremental_search(self.txtSearch.text())
def select_next_row(self) -> None: def select_next_row(self) -> None:
"""Select next or first row in playlist""" """Select next or first row in playlist"""
@ -1276,6 +1301,8 @@ class Window(QMainWindow, Ui_MainWindow):
playlist_tab = self.active_tab() playlist_tab = self.active_tab()
if playlist_tab: if playlist_tab:
playlist_tab.set_row_as_next_track() playlist_tab.set_row_as_next_track()
else:
log.error("No active tab")
def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None: def set_tab_colour(self, widget: PlaylistTab, colour: QColor) -> None:
""" """
@ -1325,7 +1352,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.tabPlaylist.setCurrentIndex(idx) self.tabPlaylist.setCurrentIndex(idx)
break break
self.tabPlaylist.currentWidget().scroll_to_top(plt.plr_rownum) display_row = self.active_proxy_model().mapFromSource(
self.active_proxy_model().source_model.index(plt.plr_rownum, 0)
).row()
self.tabPlaylist.currentWidget().scroll_to_top(display_row)
def solicit_playlist_name( def solicit_playlist_name(
self, session: scoped_session, default: str = "" self, session: scoped_session, default: str = ""
@ -1382,6 +1412,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.playing = False self.playing = False
else: else:
# Return if not playing # Return if not playing
log.error("stop_playing() called but not playing")
return return
# Stop/fade track # Stop/fade track
@ -1401,7 +1432,7 @@ class Window(QMainWindow, Ui_MainWindow):
track_sequence.now = PlaylistTrack() track_sequence.now = PlaylistTrack()
# Tell model previous track has finished # Tell model previous track has finished
self.active_model().previous_track_ended() self.active_proxy_model().previous_track_ended()
# Reset clocks # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
@ -1416,6 +1447,15 @@ class Window(QMainWindow, Ui_MainWindow):
# Enable controls # Enable controls
self.enable_play_next_controls() self.enable_play_next_controls()
def tab_change(self):
"""Called when active tab changed"""
log.info("tab_change()")
tab = self.active_tab()
if tab:
tab.resizeRowsToContents()
def tick_10ms(self) -> None: def tick_10ms(self) -> None:
""" """
Called every 10ms Called every 10ms

View File

@ -1,3 +1,5 @@
# Allow forward reference to PlaylistModel
from __future__ import annotations
import obsws_python as obs # type: ignore import obsws_python as obs # type: ignore
import re import re
from dataclasses import dataclass from dataclasses import dataclass
@ -119,6 +121,8 @@ class PlaylistModel(QAbstractTableModel):
*args, *args,
**kwargs, **kwargs,
): ):
log.info(f"PlaylistModel.__init__({playlist_id=})")
self.playlist_id = playlist_id self.playlist_id = playlist_id
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -150,6 +154,8 @@ class PlaylistModel(QAbstractTableModel):
Add track to existing header row Add track to existing header row
""" """
log.info(f"add_track_to_header({row_number=}, {track_id=}, {note=}")
# Get existing row # Get existing row
try: try:
prd = self.playlist_rows[row_number] prd = self.playlist_rows[row_number]
@ -173,10 +179,6 @@ class PlaylistModel(QAbstractTableModel):
# Add any further note (header will already have a note) # Add any further note (header will already have a note)
if note: if note:
plr.note += "\n" + note plr.note += "\n" + note
# Reset header row spanning
self.signals.span_cells_signal.emit(
self.playlist_id, row_number, HEADER_NOTES_COLUMN, 1, 1
)
# Update local copy # Update local copy
self.refresh_row(session, row_number) self.refresh_row(session, row_number)
# Repaint row # Repaint row
@ -283,6 +285,8 @@ class PlaylistModel(QAbstractTableModel):
if plr: if plr:
plr.played = True plr.played = True
self.refresh_row(session, plr.plr_rownum) self.refresh_row(session, plr.plr_rownum)
else:
log.error(f"Can't retrieve plr, {track_sequence.now.plr_id=}")
# Update track times # Update track times
self.start_end_times[row_number].start_time = track_sequence.now.start_time self.start_end_times[row_number].start_time = track_sequence.now.start_time
@ -365,6 +369,8 @@ class PlaylistModel(QAbstractTableModel):
calls. To keep it simple, if inefficient, delete rows one by one. calls. To keep it simple, if inefficient, delete rows one by one.
""" """
log.info(f"delete_rows({row_numbers=}")
with Session() as session: with Session() as session:
for row_number in row_numbers: for row_number in row_numbers:
super().beginRemoveRows(QModelIndex(), row_number, row_number) super().beginRemoveRows(QModelIndex(), row_number, row_number)
@ -380,15 +386,19 @@ class PlaylistModel(QAbstractTableModel):
Return text for display Return text for display
""" """
log.debug(f"display_role({row=}, {column=}")
# Set / reset column span
if column == HEADER_NOTES_COLUMN:
column_span = 1
if self.is_header_row(row):
column_span = self.columnCount() - 1
self.signals.span_cells_signal.emit(
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
)
if self.is_header_row(row): if self.is_header_row(row):
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:
self.signals.span_cells_signal.emit(
self.playlist_id,
row,
HEADER_NOTES_COLUMN,
1,
self.columnCount() - 1,
)
header_text = self.header_text(prd) header_text = self.header_text(prd)
if not header_text: if not header_text:
return QVariant(Config.TEXT_NO_TRACK_NO_NOTE) return QVariant(Config.TEXT_NO_TRACK_NO_NOTE)
@ -494,6 +504,8 @@ class PlaylistModel(QAbstractTableModel):
(ie, ignore the first, not-yet-duplicate, track). (ie, ignore the first, not-yet-duplicate, track).
""" """
log.info("get_duplicate_rows() called")
found = [] found = []
result = [] result = []
@ -506,6 +518,7 @@ class PlaylistModel(QAbstractTableModel):
else: else:
found.append(track_id) found.append(track_id)
log.info(f"get_duplicate_rows() returned: {result=}")
return result return result
def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int: def _get_new_row_number(self, proposed_row_number: Optional[int]) -> int:
@ -516,6 +529,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.info(f"_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)
@ -525,6 +540,7 @@ class PlaylistModel(QAbstractTableModel):
else: else:
new_row_number = proposed_row_number new_row_number = proposed_row_number
log.info(f"get_new_row_number() return: {new_row_number=}")
return new_row_number return new_row_number
def get_row_info(self, row_number: int) -> PlaylistRowData: def get_row_info(self, row_number: int) -> PlaylistRowData:
@ -557,7 +573,9 @@ class PlaylistModel(QAbstractTableModel):
Return a list of unplayed row numbers Return a list of unplayed row numbers
""" """
return [a.plr_rownum for a in self.playlist_rows.values() if not a.played] result = [a.plr_rownum for a in self.playlist_rows.values() if not a.played]
log.info(f"get_unplayed_rows() returned: {result=}")
return result
def headerData( def headerData(
self, self,
@ -716,6 +734,8 @@ class PlaylistModel(QAbstractTableModel):
Insert a row. Insert a row.
""" """
log.info(f"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)
with Session() as session: with Session() as session:
@ -779,11 +799,28 @@ class PlaylistModel(QAbstractTableModel):
return None return None
def mark_unplayed(self, row_numbers: List[int]) -> None:
"""
Mark row as unplayed
"""
with Session() as session:
for row_number in row_numbers:
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
if not plr:
return
plr.played = False
self.refresh_row(session, row_number)
self.invalidate_rows(row_numbers)
def move_rows(self, from_rows: List[int], to_row_number: int) -> None: def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
""" """
Move the playlist rows given to to_row and below. Move the playlist rows given to to_row and below.
""" """
log.info(f"move_rows({from_rows=}, {to_row_number=}")
# Build a {current_row_number: new_row_number} dictionary # Build a {current_row_number: new_row_number} dictionary
row_map: dict[int, int] = {} row_map: dict[int, int] = {}
@ -813,13 +850,6 @@ class PlaylistModel(QAbstractTableModel):
if old_row != new_row: if old_row != new_row:
row_map[old_row] = new_row row_map[old_row] = new_row
# Reset any header rows that we're moving
for moving_row in row_map:
if self.is_header_row(moving_row):
# Reset column span
self.signals.span_cells_signal.emit(
self.playlist_id, moving_row, HEADER_NOTES_COLUMN, 1, 1
)
# Check to see whether any rows in track_sequence have moved # Check to see whether any rows in track_sequence have moved
if track_sequence.previous.plr_rownum in row_map: if track_sequence.previous.plr_rownum in row_map:
track_sequence.previous.plr_rownum = row_map[ track_sequence.previous.plr_rownum = row_map[
@ -846,28 +876,19 @@ class PlaylistModel(QAbstractTableModel):
self.reset_track_sequence_row_numbers() self.reset_track_sequence_row_numbers()
self.invalidate_rows(list(row_map.keys())) self.invalidate_rows(list(row_map.keys()))
def mark_unplayed(self, row_numbers: List[int]) -> None:
"""
Mark row as unplayed
"""
with Session() as session:
for row_number in row_numbers:
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
if not plr:
return
plr.played = False
self.refresh_row(session, row_number)
self.invalidate_rows(row_numbers)
def move_rows_between_playlists( def move_rows_between_playlists(
self, from_rows: List[int], to_row_number: int, to_playlist_id: int self, from_rows: List[int], to_row_number: int, to_playlist_model: PlaylistModel
) -> None: ) -> None:
""" """
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.info(
f"move_rows_between_playlists({from_rows=}, {to_row_number=}, {to_playlist_model=}"
)
to_playlist_id = to_playlist_model.playlist_id
# Row removal must be wrapped in beginRemoveRows .. # Row removal must be wrapped in beginRemoveRows ..
# endRemoveRows and the row range must be contiguous. Process # endRemoveRows and the row range must be contiguous. Process
# the highest rows first so the lower row numbers are unchanged # the highest rows first so the lower row numbers are unchanged
@ -886,6 +907,7 @@ class PlaylistModel(QAbstractTableModel):
max_destination_row_number max_destination_row_number
and to_row_number <= max_destination_row_number and to_row_number <= max_destination_row_number
): ):
# Move the destination playlist rows down to make room.
PlaylistRows.move_rows_down( PlaylistRows.move_rows_down(
session, to_playlist_id, to_row_number, len(from_rows) session, to_playlist_id, to_row_number, len(from_rows)
) )
@ -923,6 +945,8 @@ class PlaylistModel(QAbstractTableModel):
Move existing_prd track to new_row_number and append note to any existing note Move existing_prd track to new_row_number and append note to any existing note
""" """
log.info(f"move_track_add_note({new_row_number=}, {existing_prd=}, {note=}")
if note: if note:
with Session() as session: with Session() as session:
plr = session.get(PlaylistRows, existing_prd.plrid) plr = session.get(PlaylistRows, existing_prd.plrid)
@ -944,6 +968,8 @@ class PlaylistModel(QAbstractTableModel):
Add the existing_prd track details to the existing header at header_row_number Add the existing_prd track details to the existing header at header_row_number
""" """
log.info(f"move_track_to_header({header_row_number=}, {existing_prd=}, {note=}")
if existing_prd.track_id: if existing_prd.track_id:
if note and existing_prd.note: if note and existing_prd.note:
note += "\n" + existing_prd.note note += "\n" + existing_prd.note
@ -956,6 +982,8 @@ class PlaylistModel(QAbstractTableModel):
and execute any found and execute any found
""" """
log.info(f"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):
@ -992,6 +1020,8 @@ class PlaylistModel(QAbstractTableModel):
- update display - update display
""" """
log.info("previous_track_ended()")
# Sanity check # Sanity check
if not track_sequence.previous.track_id: if not track_sequence.previous.track_id:
log.error("playlistmodel:previous_track_ended called with no current track") log.error("playlistmodel:previous_track_ended called with no current track")
@ -1025,6 +1055,8 @@ class PlaylistModel(QAbstractTableModel):
Remove track from row, retaining row as a header row Remove track from row, retaining row as a header row
""" """
log.info(f"remove_track({row_number=})")
with Session() as session: with Session() as session:
plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid) plr = session.get(PlaylistRows, self.playlist_rows[row_number].plrid)
if plr: if plr:
@ -1051,6 +1083,8 @@ class PlaylistModel(QAbstractTableModel):
Signal handler for when row ordering has changed Signal handler for when row ordering has changed
""" """
log.info("reset_track_sequence_row_numbers()")
# Check the track_sequence next, now and previous plrs and # Check the track_sequence next, now and previous plrs and
# update the row number # update the row number
with Session() as session: with Session() as session:
@ -1081,6 +1115,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.info(f"_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
@ -1095,6 +1131,7 @@ class PlaylistModel(QAbstractTableModel):
result.append(temp) result.append(temp)
result.reverse() result.reverse()
log.info(f"_reversed_contiguous_row_groups() returned: {result=}")
return result return result
def rowCount(self, index: QModelIndex = QModelIndex()) -> int: def rowCount(self, index: QModelIndex = QModelIndex()) -> int:
@ -1107,6 +1144,8 @@ class PlaylistModel(QAbstractTableModel):
Signal handler for when row ordering has changed Signal handler for when row ordering has changed
""" """
log.info(f"row_order_changed({playlist_id=}) {self.playlist_id=}")
# Only action if this is for us # Only action if this is for us
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
return return
@ -1141,6 +1180,8 @@ class PlaylistModel(QAbstractTableModel):
Set row_number as next track. If row_number is None, clear next track. Set row_number as next track. If row_number is None, clear next track.
""" """
log.info(f"set_next_row({row_number=})")
next_row_was = track_sequence.next.plr_rownum next_row_was = track_sequence.next.plr_rownum
if row_number is None: if row_number is None:
@ -1279,6 +1320,8 @@ class PlaylistModel(QAbstractTableModel):
Update track start/end times in self.playlist_rows Update track start/end times in self.playlist_rows
""" """
log.info("update_track_times()")
next_start_time: Optional[datetime] = None next_start_time: Optional[datetime] = None
update_rows: List[int] = [] update_rows: List[int] = []
@ -1360,24 +1403,27 @@ class PlaylistProxyModel(QSortFilterProxyModel):
def __init__( def __init__(
self, self,
data_model: PlaylistModel, source_model: PlaylistModel,
*args, *args,
**kwargs, **kwargs,
): ):
self.data_model = data_model self.source_model = source_model
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.setSourceModel(data_model) self.setSourceModel(source_model)
# Search all columns # Search all columns
self.setFilterKeyColumn(-1) self.setFilterKeyColumn(-1)
def __repr__(self) -> str:
return (f"<PlaylistProxyModel: source_model={self.source_model}>")
def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool: def filterAcceptsRow(self, source_row: int, source_parent: QModelIndex) -> bool:
""" """
Subclass to filter by played status Subclass to filter by played status
""" """
if self.data_model.played_tracks_hidden: if self.source_model.played_tracks_hidden:
if self.data_model.is_played_row(source_row): if self.source_model.is_played_row(source_row):
# Don't hide current or next track # Don't hide current or next track
with Session() as session: with Session() as session:
if track_sequence.next.plr_id: if track_sequence.next.plr_id:
@ -1385,7 +1431,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if ( if (
next_plr next_plr
and next_plr.plr_rownum == source_row and next_plr.plr_rownum == source_row
and next_plr.playlist_id == self.data_model.playlist_id and next_plr.playlist_id == self.source_model.playlist_id
): ):
return True return True
if track_sequence.now.plr_id: if track_sequence.now.plr_id:
@ -1393,7 +1439,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if ( if (
now_plr now_plr
and now_plr.plr_rownum == source_row and now_plr.plr_rownum == source_row
and now_plr.playlist_id == self.data_model.playlist_id and now_plr.playlist_id == self.source_model.playlist_id
): ):
return True return True
# Don't hide previous track until # Don't hide previous track until
@ -1406,7 +1452,8 @@ class PlaylistProxyModel(QSortFilterProxyModel):
if ( if (
previous_plr previous_plr
and previous_plr.plr_rownum == source_row and previous_plr.plr_rownum == source_row
and previous_plr.playlist_id == self.data_model.playlist_id and previous_plr.playlist_id
== self.source_model.playlist_id
): ):
if track_sequence.now.start_time: if track_sequence.now.start_time:
if datetime.now() > ( if datetime.now() > (
@ -1425,7 +1472,7 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# true next time through. # true next time through.
QTimer.singleShot( QTimer.singleShot(
Config.HIDE_AFTER_PLAYING_OFFSET + 100, Config.HIDE_AFTER_PLAYING_OFFSET + 100,
lambda: self.data_model.invalidate_row( lambda: self.source_model.invalidate_row(
source_row source_row
), ),
) )
@ -1452,28 +1499,28 @@ class PlaylistProxyModel(QSortFilterProxyModel):
# ###################################### # ######################################
def current_track_started(self): def current_track_started(self):
return self.data_model.current_track_started() return self.source_model.current_track_started()
def delete_rows(self, row_numbers: List[int]) -> None: def delete_rows(self, row_numbers: List[int]) -> None:
return self.data_model.delete_rows(row_numbers) return self.source_model.delete_rows(row_numbers)
def get_duplicate_rows(self) -> List[int]: def get_duplicate_rows(self) -> List[int]:
return self.data_model.get_duplicate_rows() return self.source_model.get_duplicate_rows()
def get_rows_duration(self, row_numbers: List[int]) -> int: def get_rows_duration(self, row_numbers: List[int]) -> int:
return self.data_model.get_rows_duration(row_numbers) return self.source_model.get_rows_duration(row_numbers)
def get_row_info(self, row_number: int) -> PlaylistRowData: def get_row_info(self, row_number: int) -> PlaylistRowData:
return self.data_model.get_row_info(row_number) return self.source_model.get_row_info(row_number)
def get_row_track_path(self, row_number: int) -> str: def get_row_track_path(self, row_number: int) -> str:
return self.data_model.get_row_track_path(row_number) return self.source_model.get_row_track_path(row_number)
def get_unplayed_rows(self) -> List[int]: def get_unplayed_rows(self) -> List[int]:
return self.data_model.get_unplayed_rows() return self.source_model.get_unplayed_rows()
def hide_played_tracks(self, hide: bool) -> None: def hide_played_tracks(self, hide: bool) -> None:
return self.data_model.hide_played_tracks(hide) return self.source_model.hide_played_tracks(hide)
def insert_row( def insert_row(
self, self,
@ -1481,65 +1528,65 @@ class PlaylistProxyModel(QSortFilterProxyModel):
track_id: Optional[int] = None, track_id: Optional[int] = None,
note: Optional[str] = None, note: Optional[str] = None,
) -> None: ) -> None:
return self.data_model.insert_row(proposed_row_number, track_id, note) return self.source_model.insert_row(proposed_row_number, track_id, note)
def is_header_row(self, row_number: int) -> bool: def is_header_row(self, row_number: int) -> bool:
return self.data_model.is_header_row(row_number) return self.source_model.is_header_row(row_number)
def is_played_row(self, row_number: int) -> bool: def is_played_row(self, row_number: int) -> bool:
return self.data_model.is_played_row(row_number) return self.source_model.is_played_row(row_number)
def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]: def is_track_in_playlist(self, track_id: int) -> Optional[PlaylistRowData]:
return self.data_model.is_track_in_playlist(track_id) return self.source_model.is_track_in_playlist(track_id)
def mark_unplayed(self, row_numbers: List[int]) -> None: def mark_unplayed(self, row_numbers: List[int]) -> None:
return self.data_model.mark_unplayed(row_numbers) return self.source_model.mark_unplayed(row_numbers)
def move_rows(self, from_rows: List[int], to_row_number: int) -> None: def move_rows(self, from_rows: List[int], to_row_number: int) -> None:
return self.data_model.move_rows(from_rows, to_row_number) return self.source_model.move_rows(from_rows, to_row_number)
def move_rows_between_playlists( def move_rows_between_playlists(
self, from_rows: List[int], to_row_number: int, to_playlist_id: int self, from_rows: List[int], to_row_number: int, to_playlist_id: int
) -> None: ) -> None:
return self.data_model.move_rows_between_playlists( return self.source_model.move_rows_between_playlists(
from_rows, to_row_number, to_playlist_id from_rows, to_row_number, to_playlist_id
) )
def move_track_add_note( def move_track_add_note(
self, new_row_number: int, existing_prd: PlaylistRowData, note: str self, new_row_number: int, existing_prd: PlaylistRowData, note: str
) -> None: ) -> None:
return self.data_model.move_track_add_note(new_row_number, existing_prd, note) return self.source_model.move_track_add_note(new_row_number, existing_prd, note)
def move_track_to_header( def move_track_to_header(
self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str] self, header_row_number: int, existing_prd: PlaylistRowData, note: Optional[str]
) -> None: ) -> None:
return self.data_model.move_track_to_header( return self.source_model.move_track_to_header(
header_row_number, existing_prd, note header_row_number, existing_prd, note
) )
def previous_track_ended(self) -> None: def previous_track_ended(self) -> None:
return self.data_model.previous_track_ended() return self.source_model.previous_track_ended()
def remove_track(self, row_number: int) -> None: def remove_track(self, row_number: int) -> None:
return self.data_model.remove_track(row_number) return self.source_model.remove_track(row_number)
def rescan_track(self, row_number: int) -> None: def rescan_track(self, row_number: int) -> None:
return self.data_model.rescan_track(row_number) return self.source_model.rescan_track(row_number)
def set_next_row(self, row_number: Optional[int]) -> None: def set_next_row(self, row_number: Optional[int]) -> None:
return self.data_model.set_next_row(row_number) return self.source_model.set_next_row(row_number)
def sort_by_artist(self, row_numbers: List[int]) -> None: def sort_by_artist(self, row_numbers: List[int]) -> None:
return self.data_model.sort_by_artist(row_numbers) return self.source_model.sort_by_artist(row_numbers)
def sort_by_duration(self, row_numbers: List[int]) -> None: def sort_by_duration(self, row_numbers: List[int]) -> None:
return self.data_model.sort_by_duration(row_numbers) return self.source_model.sort_by_duration(row_numbers)
def sort_by_lastplayed(self, row_numbers: List[int]) -> None: def sort_by_lastplayed(self, row_numbers: List[int]) -> None:
return self.data_model.sort_by_lastplayed(row_numbers) return self.source_model.sort_by_lastplayed(row_numbers)
def sort_by_title(self, row_numbers: List[int]) -> None: def sort_by_title(self, row_numbers: List[int]) -> None:
return self.data_model.sort_by_title(row_numbers) return self.source_model.sort_by_title(row_numbers)
def update_track_times(self) -> None: def update_track_times(self) -> None:
return self.data_model.update_track_times() return self.source_model.update_track_times()

View File

@ -40,6 +40,7 @@ from helpers import (
show_OK, show_OK,
show_warning, show_warning,
) )
from log import log
from models import Settings from models import Settings
if TYPE_CHECKING: if TYPE_CHECKING:
@ -54,9 +55,9 @@ class EscapeDelegate(QStyledItemDelegate):
- checks with user before abandoning edit on Escape - checks with user before abandoning edit on Escape
""" """
def __init__(self, parent, data_model: PlaylistModel) -> None: def __init__(self, parent, source_model: PlaylistModel) -> None:
super().__init__(parent) super().__init__(parent)
self.data_model = data_model self.source_model = source_model
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
def createEditor( def createEditor(
@ -77,7 +78,8 @@ class EscapeDelegate(QStyledItemDelegate):
row = index.row() row = index.row()
row_height = p.rowHeight(row) row_height = p.rowHeight(row)
p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT) p.setRowHeight(row, row_height + Config.MINIMUM_ROW_HEIGHT)
return QPlainTextEdit(parent) self.editor = QPlainTextEdit(parent)
return self.editor
return super().createEditor(parent, option, index) return super().createEditor(parent, option, index)
def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None: def destroyEditor(self, editor: Optional[QWidget], index: QModelIndex) -> None:
@ -102,6 +104,10 @@ class EscapeDelegate(QStyledItemDelegate):
self.closeEditor.emit(editor) self.closeEditor.emit(editor)
return True return True
elif key_event.key() == Qt.Key.Key_Escape: elif key_event.key() == Qt.Key.Key_Escape:
if self.original_text == self.editor.toPlainText():
# No changes made
self.closeEditor.emit(editor)
return True
discard_edits = QMessageBox.question( discard_edits = QMessageBox.question(
cast(QWidget, self.parent()), "Abandon edit", "Discard changes?" cast(QWidget, self.parent()), "Abandon edit", "Discard changes?"
) )
@ -111,24 +117,20 @@ class EscapeDelegate(QStyledItemDelegate):
return False return False
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
model = index.model() proxy_model = index.model()
if hasattr(model, "mapToSource"): edit_index = proxy_model.mapToSource(index)
edit_index = model.mapToSource(index)
else:
edit_index = index
value = self.data_model.data(edit_index, Qt.ItemDataRole.EditRole) self.original_text = self.source_model.data(
editor.setPlainText(value.value()) edit_index, Qt.ItemDataRole.EditRole
)
editor.setPlainText(self.original_text.value())
def setModelData(self, editor, model, index): def setModelData(self, editor, model, index):
model = index.model() proxy_model = index.model()
if hasattr(model, "mapToSource"): edit_index = proxy_model.mapToSource(index)
edit_index = model.mapToSource(index)
else:
edit_index = index
value = editor.toPlainText().strip() value = editor.toPlainText().strip()
self.data_model.setData(edit_index, value, Qt.ItemDataRole.EditRole) self.source_model.setData(edit_index, value, Qt.ItemDataRole.EditRole)
def updateEditorGeometry(self, editor, option, index): def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect) editor.setGeometry(option.rect)
@ -167,11 +169,12 @@ class PlaylistTab(QTableView):
# Save passed settings # Save passed settings
self.musicmuster = musicmuster self.musicmuster = musicmuster
self.playlist_id = playlist_id self.playlist_id = playlist_id
log.info(f"PlaylistTab.__init__({playlist_id=})")
# Set up widget # Set up widget
self.data_model = PlaylistModel(playlist_id) self.source_model = PlaylistModel(playlist_id)
self.proxy_model = PlaylistProxyModel(self.data_model) self.proxy_model = PlaylistProxyModel(self.source_model)
self.setItemDelegate(EscapeDelegate(self, self.data_model)) self.setItemDelegate(EscapeDelegate(self, self.source_model))
self.setAlternatingRowColors(True) self.setAlternatingRowColors(True)
self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel) self.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove) self.setDragDropMode(QAbstractItemView.DragDropMode.InternalMove)
@ -188,14 +191,8 @@ class PlaylistTab(QTableView):
self.customContextMenuRequested.connect(self._context_menu) self.customContextMenuRequested.connect(self._context_menu)
# Connect signals # Connect signals
# This dancing is to satisfy mypy
h_header = self.horizontalHeader()
if isinstance(h_header, QHeaderView):
h_header.sectionResized.connect(self._column_resize)
h_header.setStretchLastSection(True)
# self.signals.set_next_track_signal.connect(self._reset_next)
self.signals = MusicMusterSignals() self.signals = MusicMusterSignals()
self.signals.resize_rows_signal.connect(self.resizeRowsToContents) self.signals.resize_rows_signal.connect(self.resize_rows)
self.signals.span_cells_signal.connect(self._span_cells) self.signals.span_cells_signal.connect(self._span_cells)
# Selection model # Selection model
@ -205,9 +202,20 @@ class PlaylistTab(QTableView):
# Load playlist rows # Load playlist rows
self.setModel(self.proxy_model) self.setModel(self.proxy_model)
self._set_column_widths() self._set_column_widths()
QTimer.singleShot(0, lambda: self.resizeRowsToContents()) # Stretch last column *after* setting column widths which is
# *much* faster
h_header = self.horizontalHeader()
if isinstance(h_header, QHeaderView):
h_header.sectionResized.connect(self._column_resize)
h_header.setStretchLastSection(True)
# Setting ResizeToContents causes screen flash on load
# v_header = self.verticalHeader()
# if v_header:
# print("HEADER")
# v_header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents)
QTimer.singleShot(300, self.resizeRowsToContents)
# ########## Overrident class functions ########## # ########## Overridden class functions ##########
def closeEditor( def closeEditor(
self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint self, editor: QWidget | None, hint: QAbstractItemDelegate.EndEditHint
@ -225,7 +233,7 @@ class PlaylistTab(QTableView):
# Update start times in case a start time in a note has been # Update start times in case a start time in a note has been
# edited # edited
self.data_model.update_track_times() self.source_model.update_track_times()
# Deselect edited line # Deselect edited line
self.clear_selection() self.clear_selection()
@ -240,13 +248,14 @@ class PlaylistTab(QTableView):
from_rows = self.selected_model_row_numbers() from_rows = self.selected_model_row_numbers()
to_index = self.indexAt(event.position().toPoint()) to_index = self.indexAt(event.position().toPoint())
to_model_row = self.proxy_model.mapToSource(to_index).row() to_model_row = self.proxy_model.mapToSource(to_index).row()
log.info(f"PlaylistTab.dropEvent(): {from_rows=}, {to_index=}, {to_model_row=}")
if ( if (
0 <= min(from_rows) <= self.data_model.rowCount() 0 <= min(from_rows) <= self.source_model.rowCount()
and 0 <= max(from_rows) <= self.data_model.rowCount() and 0 <= max(from_rows) <= self.source_model.rowCount()
and 0 <= to_model_row <= self.data_model.rowCount() and 0 <= to_model_row <= self.source_model.rowCount()
): ):
self.data_model.move_rows(from_rows, to_model_row) self.source_model.move_rows(from_rows, to_model_row)
# Reset drag mode to allow row selection by dragging # Reset drag mode to allow row selection by dragging
self.setDragEnabled(False) self.setDragEnabled(False)
@ -283,7 +292,7 @@ class PlaylistTab(QTableView):
if len(selected_rows) == 0: if len(selected_rows) == 0:
self.musicmuster.lblSumPlaytime.setText("") self.musicmuster.lblSumPlaytime.setText("")
else: else:
selected_duration = self.data_model.get_rows_duration( selected_duration = self.source_model.get_rows_duration(
self.get_selected_rows() self.get_selected_rows()
) )
if selected_duration > 0: if selected_duration > 0:
@ -321,7 +330,7 @@ class PlaylistTab(QTableView):
def _add_track(self) -> None: def _add_track(self) -> None:
"""Add a track to a section header making it a normal track row""" """Add a track to a section header making it a normal track row"""
model_row_number = self.selected_model_row_number() model_row_number = self.source_model_selected_row_number()
if model_row_number is None: if model_row_number is None:
return return
@ -329,7 +338,7 @@ class PlaylistTab(QTableView):
dlg = TrackSelectDialog( dlg = TrackSelectDialog(
session=session, session=session,
new_row_number=model_row_number, new_row_number=model_row_number,
model=self.data_model, source_model=self.source_model,
add_to_header=True, add_to_header=True,
) )
dlg.exec() dlg.exec()
@ -338,24 +347,28 @@ class PlaylistTab(QTableView):
"""Used to process context (right-click) menu, which is defined here""" """Used to process context (right-click) menu, which is defined here"""
self.menu.clear() self.menu.clear()
model = self.proxy_model proxy_model = self.proxy_model
display_row_number = item.row() index = proxy_model.index(item.row(), item.column())
if hasattr(model, "mapToSource"): model_row_number = proxy_model.mapToSource(index).row()
index = model.index(item.row(), item.column())
model_row_number = model.mapToSource(index).row()
else:
model_row_number = display_row_number
header_row = model.is_header_row(model_row_number) header_row = proxy_model.is_header_row(model_row_number)
track_row = not header_row track_row = not header_row
current_row = model_row_number == track_sequence.now.plr_rownum current_row = model_row_number == track_sequence.now.plr_rownum
next_row = model_row_number == track_sequence.next.plr_rownum next_row = model_row_number == track_sequence.next.plr_rownum
track_path = self.source_model.get_row_info(model_row_number).path
# Open in Audacity # Open/import in/from Audacity
if track_row and not current_row: if track_row and not current_row:
if track_path == self.musicmuster.audacity_file_path:
# This track was opened in Audacity
self._add_context_menu( self._add_context_menu(
"Open in Audacity", lambda: self.open_in_audacity(model_row_number) "Update from Audacity",
lambda: self._import_from_audacity(model_row_number),
)
else:
self._add_context_menu(
"Open in Audacity", lambda: self._open_in_audacity(model_row_number)
) )
# Rescan # Rescan
@ -374,7 +387,8 @@ class PlaylistTab(QTableView):
# Remove track from row # Remove track from row
if track_row and not current_row and not next_row: if track_row and not current_row and not next_row:
self._add_context_menu( self._add_context_menu(
"Remove track from row", lambda: model.remove_track(model_row_number) "Remove track from row",
lambda: proxy_model.remove_track(model_row_number),
) )
# Add track to section header (ie, make this a track row) # Add track to section header (ie, make this a track row)
@ -385,7 +399,7 @@ class PlaylistTab(QTableView):
self.menu.addSeparator() self.menu.addSeparator()
# Mark unplayed # Mark unplayed
if track_row and model.is_played_row(model_row_number): if track_row and proxy_model.is_played_row(model_row_number):
self._add_context_menu( self._add_context_menu(
"Mark unplayed", "Mark unplayed",
lambda: self._mark_as_unplayed(self.get_selected_rows()), lambda: self._mark_as_unplayed(self.get_selected_rows()),
@ -404,22 +418,22 @@ class PlaylistTab(QTableView):
sort_menu = self.menu.addMenu("Sort") sort_menu = self.menu.addMenu("Sort")
self._add_context_menu( self._add_context_menu(
"by title", "by title",
lambda: model.sort_by_title(self.get_selected_rows()), lambda: proxy_model.sort_by_title(self.get_selected_rows()),
parent_menu=sort_menu, parent_menu=sort_menu,
) )
self._add_context_menu( self._add_context_menu(
"by artist", "by artist",
lambda: model.sort_by_artist(self.get_selected_rows()), lambda: proxy_model.sort_by_artist(self.get_selected_rows()),
parent_menu=sort_menu, parent_menu=sort_menu,
) )
self._add_context_menu( self._add_context_menu(
"by duration", "by duration",
lambda: model.sort_by_duration(self.get_selected_rows()), lambda: proxy_model.sort_by_duration(self.get_selected_rows()),
parent_menu=sort_menu, parent_menu=sort_menu,
) )
self._add_context_menu( self._add_context_menu(
"by last played", "by last played",
lambda: model.sort_by_lastplayed(self.get_selected_rows()), lambda: proxy_model.sort_by_lastplayed(self.get_selected_rows()),
parent_menu=sort_menu, parent_menu=sort_menu,
) )
@ -449,6 +463,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.info(f"_column_resize({column_number=}, {_old=}, {_new=}")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:
return return
@ -474,7 +490,7 @@ class PlaylistTab(QTableView):
to the clipboard. Otherwise, return None. to the clipboard. Otherwise, return None.
""" """
track_path = self.data_model.get_row_info(row_number).path track_path = self.source_model.get_row_info(row_number).path
if not track_path: if not track_path:
return return
@ -502,6 +518,7 @@ class PlaylistTab(QTableView):
""" """
rows_to_delete = self.get_selected_rows() rows_to_delete = self.get_selected_rows()
log.info(f"_delete_rows({rows_to_delete=}")
row_count = len(rows_to_delete) row_count = len(rows_to_delete)
if row_count < 1: if row_count < 1:
return return
@ -511,7 +528,7 @@ class PlaylistTab(QTableView):
if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"): if not ask_yes_no("Delete rows", f"Really delete {row_count} row{plural}?"):
return return
self.data_model.delete_rows(self.selected_model_row_numbers()) self.source_model.delete_rows(self.selected_model_row_numbers())
self.clear_selection() self.clear_selection()
def get_selected_row_track_path(self) -> str: def get_selected_row_track_path(self) -> str:
@ -520,17 +537,25 @@ class PlaylistTab(QTableView):
row does not have a track, return empty string. row does not have a track, return empty string.
""" """
model_row_number = self.selected_model_row_number() log.info("get_selected_row_track_path() called")
model_row_number = self.source_model_selected_row_number()
if model_row_number is None: if model_row_number is None:
return "" result = ""
return self.data_model.get_row_track_path(model_row_number) else:
result = self.source_model.get_row_track_path(model_row_number)
log.info(f"get_selected_row_track_path() returned: {result=}")
return result
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"""
log.info("get_selected_rows() called")
# Use a set to deduplicate result (a selected row will have all # Use a set to deduplicate result (a selected row will have all
# items in that row selected) # items in that row selected)
return sorted( result = sorted(
list( list(
set( set(
[ [
@ -541,10 +566,31 @@ class PlaylistTab(QTableView):
) )
) )
log.info(f"get_selected_rows() returned: {result=}")
return result
def _import_from_audacity(self, row_number: int) -> None:
"""
Import current Audacity track to passed row
"""
# Notify user if audacity not running
if "audacity" not in [i.name() for i in psutil.process_iter()]:
show_warning(self.musicmuster, "Audacity", "Audacity is not running")
return
audacity = self.audacity
if not audacity:
return
audacity.export_file()
self.musicmuster.audacity_file_path = None
self._rescan(row_number)
def _info_row(self, row_number: int) -> None: def _info_row(self, row_number: int) -> None:
"""Display popup with info re row""" """Display popup with info re row"""
prd = self.data_model.get_row_info(row_number) prd = self.source_model.get_row_info(row_number)
if prd: if prd:
txt = ( txt = (
f"Title: {prd.title}\n" f"Title: {prd.title}\n"
@ -561,12 +607,12 @@ class PlaylistTab(QTableView):
show_OK(self.musicmuster, "Track info", txt) show_OK(self.musicmuster, "Track info", txt)
def _mark_as_unplayed(self, row_numbers: List[int]) -> None: def _mark_as_unplayed(self, row_numbers: List[int]) -> None:
"""Rescan track""" """Mark row as unplayed"""
self.data_model.mark_unplayed(row_numbers) self.source_model.mark_unplayed(row_numbers)
self.clear_selection() self.clear_selection()
def open_in_audacity(self, row_number: int) -> None: def _open_in_audacity(self, row_number: int) -> None:
""" """
Open track in passed row in Audacity Open track in passed row in Audacity
""" """
@ -576,26 +622,32 @@ class PlaylistTab(QTableView):
show_warning(self.musicmuster, "Audacity", "Audacity is not running") show_warning(self.musicmuster, "Audacity", "Audacity is not running")
return return
path = self.data_model.get_row_track_path(row_number) path = self.source_model.get_row_track_path(row_number)
if not path: if not path:
return return
audacity = AudacityManager() self.musicmuster.audacity_file_path = path
audacity.open_file(path) self.audacity = AudacityManager()
if ask_yes_no( self.audacity.open_file(path)
"Export file",
"Click yes to export file, no to ignore",
parent=self.musicmuster,
):
audacity.export_file()
self._rescan(row_number)
def _rescan(self, row_number: int) -> None: def _rescan(self, row_number: int) -> None:
"""Rescan track""" """Rescan track"""
self.data_model.rescan_track(row_number) self.source_model.rescan_track(row_number)
self.clear_selection() self.clear_selection()
def resize_rows(self, playlist_id: int) -> None:
"""
If playlist_id is us, resize rows
"""
log.info(f"resize_rows({playlist_id=}) {self.playlist_id=}")
if playlist_id != self.playlist_id:
return
self.resizeRowsToContents()
def scroll_to_top(self, row_number: int) -> None: def scroll_to_top(self, row_number: int) -> None:
""" """
Scroll to put passed row_number Config.SCROLL_TOP_MARGIN from the Scroll to put passed row_number Config.SCROLL_TOP_MARGIN from the
@ -620,14 +672,14 @@ class PlaylistTab(QTableView):
# We need to be in MultiSelection mode # We need to be in MultiSelection mode
self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.MultiSelection)
# Get the duplicate rows # Get the duplicate rows
duplicate_rows = self.data_model.get_duplicate_rows() duplicate_rows = self.source_model.get_duplicate_rows()
# Select the rows # Select the rows
for duplicate_row in duplicate_rows: for duplicate_row in duplicate_rows:
self.selectRow(duplicate_row) self.selectRow(duplicate_row)
# Reset selection mode # Reset selection mode
self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection) self.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
def selected_model_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
""" """
@ -635,9 +687,7 @@ class PlaylistTab(QTableView):
selected_index = self._selected_row_index() selected_index = self._selected_row_index()
if selected_index is None: if selected_index is None:
return None return None
if hasattr(self.proxy_model, "mapToSource"):
return self.proxy_model.mapToSource(selected_index).row() return self.proxy_model.mapToSource(selected_index).row()
return selected_index.row()
def selected_model_row_numbers(self) -> List[int]: def selected_model_row_numbers(self) -> List[int]:
""" """
@ -682,13 +732,15 @@ class PlaylistTab(QTableView):
def _set_column_widths(self) -> None: def _set_column_widths(self) -> None:
"""Column widths from settings""" """Column widths from settings"""
log.info("_set_column_widths()")
header = self.horizontalHeader() header = self.horizontalHeader()
if not header: if not header:
return return
# Last column is set to stretch so ignore it here # Last column is set to stretch so ignore it here
with Session() as session: with Session() as session:
for column_number in range(header.count() - 2): for column_number in range(header.count() - 1):
attr_name = f"playlist_col_{column_number}_width" attr_name = f"playlist_col_{column_number}_width"
record = Settings.get_int_settings(session, attr_name) record = Settings.get_int_settings(session, attr_name)
if record.f_int is not None: if record.f_int is not None:
@ -701,10 +753,11 @@ class PlaylistTab(QTableView):
Set selected row as next track Set selected row as next track
""" """
model_row_number = self.selected_model_row_number() model_row_number = self.source_model_selected_row_number()
log.info(f"set_row_as_next_track() {model_row_number=}")
if model_row_number is None: if model_row_number is None:
return return
self.data_model.set_next_row(model_row_number) self.source_model.set_next_row(model_row_number)
self.clearSelection() self.clearSelection()
def _span_cells( def _span_cells(
@ -714,12 +767,18 @@ class PlaylistTab(QTableView):
Implement spanning of cells, initiated by signal Implement spanning of cells, initiated by signal
""" """
log.debug(
f"_span_cells({playlist_id=}, {row=}, "
f"{column=}, {rowSpan=}, {columnSpan=}) {self.playlist_id=}"
)
if playlist_id != self.playlist_id: if playlist_id != self.playlist_id:
return return
model = self.proxy_model proxy_model = self.proxy_model
if hasattr(model, "mapToSource"): edit_index = proxy_model.mapFromSource(
edit_index = model.mapFromSource(self.data_model.createIndex(row, column)) self.source_model.createIndex(row, column)
)
row = edit_index.row() row = edit_index.row()
column = edit_index.column() column = edit_index.column()
@ -737,5 +796,5 @@ class PlaylistTab(QTableView):
def _unmark_as_next(self) -> None: def _unmark_as_next(self) -> None:
"""Rescan track""" """Rescan track"""
self.data_model.set_next_row(None) self.source_model.set_next_row(None)
self.clear_selection() self.clear_selection()

22
poetry.lock generated
View File

@ -274,7 +274,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
name = "colorama" name = "colorama"
version = "0.4.6" version = "0.4.6"
description = "Cross-platform colored terminal text." description = "Cross-platform colored terminal text."
category = "dev" category = "main"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [ files = [
@ -282,6 +282,24 @@ files = [
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
] ]
[[package]]
name = "colorlog"
version = "6.8.0"
description = "Add colours to the output of Python's logging module."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
{file = "colorlog-6.8.0-py3-none-any.whl", hash = "sha256:4ed23b05a1154294ac99f511fabe8c1d6d4364ec1f7fc989c7fb515ccc29d375"},
{file = "colorlog-6.8.0.tar.gz", hash = "sha256:fbb6fdf9d5685f2517f388fb29bb27d54e8654dd31f58bc2a3b217e967a95ca6"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
[[package]] [[package]]
name = "decorator" name = "decorator"
version = "5.1.1" version = "5.1.1"
@ -2189,4 +2207,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.9" python-versions = "^3.9"
content-hash = "5bd0a9ae09f61079a0325639485adb206357cd5ea942944ccb5855f2a83d4db6" content-hash = "3ba4a6affcb5c77a3c0e4b0f7c12d2b7f5d192a68bf7e71eb6a4e58024b5f4e7"

View File

@ -26,6 +26,7 @@ pyqt6 = "^6.5.0"
pyqt6-webengine = "^6.5.0" pyqt6-webengine = "^6.5.0"
pygame = "^2.4.0" pygame = "^2.4.0"
pyqtgraph = "^0.13.3" pyqtgraph = "^0.13.3"
colorlog = "^6.8.0"
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
ipdb = "^0.13.9" ipdb = "^0.13.9"

View File

@ -303,7 +303,7 @@ def test_move_one_row_between_playlists_to_end(monkeypatch, session):
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
model_dst.refresh_data(session) model_dst.refresh_data(session)
assert len(model_src.playlist_rows) == create_rowcount - len(from_rows) assert len(model_src.playlist_rows) == create_rowcount - len(from_rows)
@ -323,7 +323,7 @@ def test_move_one_row_between_playlists_to_middle(monkeypatch, session):
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
model_dst.refresh_data(session) model_dst.refresh_data(session)
# Check the rows of the destination model # Check the rows of the destination model
@ -347,7 +347,7 @@ def test_move_multiple_rows_between_playlists_to_end(monkeypatch, session):
model_src = create_model_with_playlist_rows(session, create_rowcount, name="source") model_src = create_model_with_playlist_rows(session, create_rowcount, name="source")
model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination") model_dst = create_model_with_playlist_rows(session, create_rowcount, name="destination")
model_src.move_rows_between_playlists(from_rows, to_row, model_dst.playlist_id) model_src.move_rows_between_playlists(from_rows, to_row, model_dst)
model_dst.refresh_data(session) model_dst.refresh_data(session)
# Check the rows of the destination model # Check the rows of the destination model