diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models.py b/app/models.py index f50f8e6..94de36f 100644 --- a/app/models.py +++ b/app/models.py @@ -7,7 +7,7 @@ import stackprinter # type: ignore from dbconfig import Session, scoped_session from datetime import datetime -from typing import List, Optional +from typing import Iterable, List, Optional from sqlalchemy.ext.associationproxy import association_proxy @@ -95,13 +95,13 @@ class NoteColours(Base): ) @staticmethod - def get_colour(session: scoped_session, text: str) -> Optional[str]: + def get_colour(session: scoped_session, text: str) -> str: """ - Parse text and return colour string if matched, else None + Parse text and return colour string if matched, else empty string """ if not text: - return None + return "" for rec in session.execute( select(NoteColours) @@ -123,7 +123,7 @@ class NoteColours(Base): if rec.substring.lower() in text.lower(): return rec.colour - return None + return "" class Playdates(Base): @@ -196,7 +196,7 @@ class Playlists(Base): is_template = Column(Boolean, default=False, nullable=False) query = Column(String(256), default=None, nullable=True, unique=False) deleted = Column(Boolean, default=False, nullable=False) - rows: "PlaylistRows" = relationship( + rows: List["PlaylistRows"] = relationship( "PlaylistRows", back_populates="playlist", cascade="all, delete-orphan", @@ -370,7 +370,7 @@ class PlaylistRows(Base): def __init__(self, session: scoped_session, playlist_id: int, - track_id: int, + track_id: Optional[int], row_number: int, note: Optional[str] = None ) -> None: @@ -409,7 +409,7 @@ class PlaylistRows(Base): @staticmethod def delete_plrids_not_in_list(session: scoped_session, playlist_id: int, - plrids: List["PlaylistRows"]) -> None: + plrids: List[int]) -> None: """ Delete rows in given playlist that have a higher row number than 'maxrow' @@ -469,7 +469,7 @@ class PlaylistRows(Base): @classmethod def get_played_rows(cls, session: scoped_session, - playlist_id: int) -> List[int]: + playlist_id: int) -> List["PlaylistRows"]: """ For passed playlist, return a list of rows that have been played. @@ -488,7 +488,7 @@ class PlaylistRows(Base): @classmethod def get_rows_with_tracks(cls, session: scoped_session, - playlist_id: int) -> List[int]: + playlist_id: int) -> List["PlaylistRows"]: """ For passed playlist, return a list of rows that contain tracks @@ -526,24 +526,8 @@ class PlaylistRows(Base): return plrs @staticmethod - def move_rows_down(session: scoped_session, playlist_id: int, - starting_row: int, move_by: int) -> None: - """ - Create space to insert move_by additional rows by incremented row - number from starting_row to end of playlist - """ - - session.execute( - update(PlaylistRows) - .where( - (PlaylistRows.playlist_id == playlist_id), - (PlaylistRows.row_number >= starting_row) - ) - .values(row_number=PlaylistRows.row_number + move_by) - ) - - @staticmethod - def indexed_by_id(session: scoped_session, plr_ids: List[int]) -> dict: + def indexed_by_id(session: scoped_session, + plr_ids: Iterable[int]) -> dict: """ Return a dictionary of playlist_rows indexed by their plr id from the passed plr_id list. @@ -562,6 +546,23 @@ class PlaylistRows(Base): return result + @staticmethod + def move_rows_down(session: scoped_session, playlist_id: int, + starting_row: int, move_by: int) -> None: + """ + Create space to insert move_by additional rows by incremented row + number from starting_row to end of playlist + """ + + session.execute( + update(PlaylistRows) + .where( + (PlaylistRows.playlist_id == playlist_id), + (PlaylistRows.row_number >= starting_row) + ) + .values(row_number=PlaylistRows.row_number + move_by) + ) + class Settings(Base): """Manage settings""" diff --git a/app/musicmuster.py b/app/musicmuster.py index 6f5d3e8..6e4f0d0 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -9,10 +9,29 @@ import threading from datetime import datetime, timedelta from time import sleep -from typing import Callable, List, Optional +from typing import ( + Callable, + cast, + List, + Optional, +) -from PyQt5.QtCore import pyqtSignal, QDate, QEvent, Qt, QSize, QTime, QTimer -from PyQt5.QtGui import QColor, QFont, QPalette, QResizeEvent +from PyQt5.QtCore import ( + pyqtSignal, + QDate, + QEvent, + Qt, + QSize, + QTime, + QTimer, +) +from PyQt5.QtGui import ( + QColor, + QFont, + QMouseEvent, + QPalette, + QResizeEvent, +) from PyQt5.QtWidgets import ( QApplication, QDialog, @@ -27,10 +46,13 @@ from PyQt5.QtWidgets import ( QProgressBar, ) -from dbconfig import engine, Session, scoped_session +from dbconfig import ( + engine, + Session, + scoped_session, +) import helpers import music - from models import ( Base, Carts, @@ -55,12 +77,11 @@ class CartButton(QPushButton): progress = pyqtSignal(int) - def __init__(self, parent: QMainWindow, cart: Carts): + def __init__(self, musicmuster: "Window", cart: Carts, *args, **kwargs): """Create a cart pushbutton and set it disabled""" - super().__init__(parent) - # Next line is redundant (check) - # self.parent = parent + super().__init__(*args, **kwargs) + self.musicmuster = musicmuster self.cart_id = cart.id if cart.path and cart.enabled and not cart.duration: tags = helpers.get_tags(cart.path) @@ -101,8 +122,9 @@ class CartButton(QPushButton): """Allow right click even when button is disabled""" if event.type() == QEvent.MouseButtonRelease: - if event.button() == Qt.RightButton: - self.parent.cart_edit(self, event) + mouse_event = cast(QMouseEvent, event) + if mouse_event.button() == Qt.RightButton: + self.musicmuster.cart_edit(self, event) # type: ignore # FIXME return True return super().event(event) @@ -137,7 +159,7 @@ class PlaylistTrack: self.playlist_id: Optional[int] = None self.playlist_tab: Optional[PlaylistTab] = None self.plr_id: Optional[int] = None - self.silence_at: Optional[datetime] = None + self.silence_at: Optional[int] = None self.start_gap: Optional[int] = None self.start_time: Optional[datetime] = None self.title: Optional[str] = None @@ -164,7 +186,6 @@ class PlaylistTrack: self.duration = track.duration self.end_time = None self.fade_at = track.fade_at - self.fade_length = track.silence_at - track.fade_at self.path = track.path self.playlist_id = plr.playlist_id self.plr_id = plr.id @@ -174,18 +195,23 @@ class PlaylistTrack: self.title = track.title self.track_id = track.id + if track.silence_at and track.fade_at: + self.fade_length = track.silence_at - track.fade_at + def start(self) -> None: """ Called when track starts playing """ self.start_time = datetime.now() - self.end_time = self.start_time + timedelta(milliseconds=self.duration) + if self.duration: + self.end_time = ( + self.start_time + timedelta(milliseconds=self.duration)) class Window(QMainWindow, Ui_MainWindow): - def __init__(self, parent=None) -> None: - super().__init__(parent) + def __init__(self, parent=None, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) self.setupUi(self) self.timer: QTimer = QTimer() @@ -198,8 +224,8 @@ class Window(QMainWindow, Ui_MainWindow): self.next_track = PlaylistTrack() self.previous_track = PlaylistTrack() - self.previous_track_position: Optional[int] = None - self.selected_plrs = None + self.previous_track_position: Optional[float] = None + self.selected_plrs: Optional[List[PlaylistRows]] = None # Set colours that will be used by playlist row stripes palette = QPalette() @@ -239,7 +265,8 @@ class Window(QMainWindow, Ui_MainWindow): colour = Config.COLOUR_CART_READY btn.path = cart.path btn.player = self.music.VLC.media_player_new(cart.path) - btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT) + if btn.player: + btn.player.audio_set_volume(Config.VOLUME_VLC_DEFAULT) if cart.enabled: btn.setEnabled(True) btn.pgb.setVisible(True) @@ -249,16 +276,24 @@ class Window(QMainWindow, Ui_MainWindow): colour = Config.COLOUR_CART_UNCONFIGURED btn.setStyleSheet("background-color: " + colour + ";\n") - btn.setText(cart.name) + if cart.name is not None: + btn.setText(cart.name) def cart_click(self) -> None: """Handle cart click""" btn = self.sender() + if not isinstance(btn, CartButton): + return if helpers.file_is_readable(btn.path): # Don't allow clicks while we're playing btn.setEnabled(False) + if not btn.player: + log.debug( + f"musicmuster.cart_click(): no player assigned ({btn=})") + return + btn.player.play() btn.is_playing = True colour = Config.COLOUR_CART_PLAYING @@ -268,7 +303,7 @@ class Window(QMainWindow, Ui_MainWindow): else: colour = Config.COLOUR_CART_ERROR btn.setStyleSheet("background-color: " + colour + ";\n") - btn.pgb.minimum = 0 + btn.pgb.setMinimum(0) def cart_edit(self, btn: CartButton, event: QEvent): """Handle context menu for cart button""" @@ -276,10 +311,10 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: cart = session.query(Carts).get(btn.cart_id) if cart is None: - log.ERROR("cart_edit: cart not found") + log.error("cart_edit: cart not found") return - dlg = CartDialog(parent=self, session=session, cart=cart) + dlg = CartDialog(musicmuster=self, session=session, cart=cart) if dlg.exec(): name = dlg.ui.lineEditName.text() if not name: @@ -324,6 +359,9 @@ class Window(QMainWindow, Ui_MainWindow): def cart_progressbar(self, btn: CartButton) -> None: """Manage progress bar""" + if not btn.duration: + return + ms = 0 btn.pgb.setMaximum(btn.duration) while ms <= btn.duration: @@ -437,7 +475,8 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: playlist_id = self.tabPlaylist.widget(tab_index).playlist_id playlist = session.get(Playlists, playlist_id) - playlist.close(session) + if playlist: + playlist.close(session) # Close playlist and remove tab self.tabPlaylist.widget(tab_index).close() @@ -498,10 +537,8 @@ class Window(QMainWindow, Ui_MainWindow): playlist_name: Optional[str] = None) -> Playlists: """Create new playlist""" - if not playlist_name: + while not playlist_name: playlist_name = self.solicit_playlist_name() - if not playlist_name: - return playlist = Playlists(session, playlist_name) return playlist @@ -521,6 +558,8 @@ class Window(QMainWindow, Ui_MainWindow): add tab to display. Return index number of tab. """ + assert playlist.id + playlist_tab = PlaylistTab( musicmuster=self, session=session, playlist_id=playlist.id) idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) @@ -622,7 +661,8 @@ class Window(QMainWindow, Ui_MainWindow): # Repaint playlist to remove currently playing track colour # What was current track is now previous track with Session() as session: - self.previous_track.playlist_tab.update_display(session) + if self.previous_track.playlist_tab: + self.previous_track.playlist_tab.update_display(session) # Reset clocks self.frame_fade.setStyleSheet("") @@ -638,8 +678,11 @@ class Window(QMainWindow, Ui_MainWindow): self.label_track_length.setText( helpers.ms_to_mmss(self.next_track.duration) ) - self.label_fade_length.setText(helpers.ms_to_mmss( - self.next_track.silence_at - self.next_track.fade_at)) + if self.next_track.silence_at and self.next_track.fade_at: + self.label_fade_length.setText(helpers.ms_to_mmss( + self.next_track.silence_at - self.next_track.fade_at)) + else: + self.label_fade_length.setText("0:00") else: self.label_track_length.setText("0:00") self.label_fade_length.setText("0:00") @@ -662,6 +705,9 @@ class Window(QMainWindow, Ui_MainWindow): # Get output filename playlist = session.get(Playlists, playlist_id) + if not playlist: + return + pathspec = QFileDialog.getSaveFileName( self, 'Save Playlist', directory=f"{playlist.name}.m3u", @@ -679,6 +725,8 @@ class Window(QMainWindow, Ui_MainWindow): # Required directive on first line f.write("#EXTM3U\n") for track in [a.track for a in plrs]: + if track.duration is None: + track.duration = 0 f.write( "#EXTINF:" f"{int(track.duration / 1000)}," @@ -707,7 +755,8 @@ class Window(QMainWindow, Ui_MainWindow): git_tag = str(exc_info.output) with Session() as session: - dbname = session.bind.engine.url.database + if session.bind: + dbname = session.bind.engine.url.database QMessageBox.information( self, @@ -721,7 +770,9 @@ class Window(QMainWindow, Ui_MainWindow): dlg = DbDialog(self, session, get_one_track=True) if dlg.exec(): - return dlg.ui.track + return dlg.track + else: + return None def hide_played(self): """Toggle hide played tracks""" @@ -832,7 +883,8 @@ class Window(QMainWindow, Ui_MainWindow): with Session() as session: for playlist in Playlists.get_open(session): - _ = self.create_playlist_tab(session, playlist) + if playlist: + _ = self.create_playlist_tab(session, playlist) # Set active tab record = Settings.get_int_settings(session, "active_tab") if record and record.f_int >= 0: @@ -854,11 +906,14 @@ class Window(QMainWindow, Ui_MainWindow): # Remove current/next rows from list plrs_to_move = [plr for plr in playlistrows if plr.id not in - [self.current_track.plr_id, - self.next_track.plr_id] + [self.current_track.plr_id, + self.next_track.plr_id] ] - rows_to_delete = [plr.row_number for plr in plrs_to_move] + rows_to_delete = [plr.row_number for plr in plrs_to_move + if plr.row_number is not None] + if not rows_to_delete: + return # Identify destination playlist playlists = [] @@ -912,11 +967,13 @@ class Window(QMainWindow, Ui_MainWindow): Move selected rows to another playlist """ + selected_plrs = self.visible_playlist_tab().get_selected_playlistrows( + session) + if not selected_plrs: + return + with Session() as session: - self.move_playlist_rows( - session, - self.visible_playlist_tab().get_selected_playlistrows(session) - ) + self.move_playlist_rows(session, selected_plrs) def move_tab(self, frm: int, to: int) -> None: """Handle tabs being moved""" @@ -954,6 +1011,8 @@ class Window(QMainWindow, Ui_MainWindow): return playlist = Playlists.create_playlist_from_template( session, template, playlist_name) + if not playlist: + return tab_index = self.create_playlist_tab(session, playlist) playlist.mark_open(session, tab_index) @@ -1007,7 +1066,10 @@ class Window(QMainWindow, Ui_MainWindow): plr.row_number = row row += 1 - session.commit() + if not src_playlist_id: + return + + session.flush() # Update display self.visible_playlist_tab().populate_display( @@ -1063,8 +1125,9 @@ class Window(QMainWindow, Ui_MainWindow): # Ensure playlist tabs are the correct colour # If next track is on a different playlist_tab to the # current track, reset the current track playlist_tab colour - if self.current_track.playlist_tab != self.next_track.playlist_tab: - self.set_tab_colour(self.current_track.playlist_tab, + current_tab = self.current_track.playlist_tab + if current_tab and current_tab != self.next_track.playlist_tab: + self.set_tab_colour(current_tab, QColor(Config.COLOUR_NORMAL_TAB)) # Move next track to current track. @@ -1073,9 +1136,17 @@ class Window(QMainWindow, Ui_MainWindow): self.current_track = self.next_track self.next_track = PlaylistTrack() + if not self.current_track.track_id: + log.debug("musicmuster.play_next(): no id for next track") + return + if not self.current_track.path: + log.debug("musicmuster.play_next(): no path for next track") + return + # Set current track playlist_tab colour - self.set_tab_colour(self.current_track.playlist_tab, - QColor(Config.COLOUR_CURRENT_TAB)) + if current_tab: + self.set_tab_colour( + current_tab, QColor(Config.COLOUR_CURRENT_TAB)) # Restore volume if -3dB active if self.btnDrop3db.isChecked(): @@ -1089,7 +1160,8 @@ class Window(QMainWindow, Ui_MainWindow): Playdates(session, self.current_track.track_id) # Tell playlist track is now playing - self.current_track.playlist_tab.play_started(session) + if self.current_track.playlist_tab: + self.current_track.playlist_tab.play_started(session) # Note that track is now playing self.playing = True @@ -1116,11 +1188,14 @@ class Window(QMainWindow, Ui_MainWindow): ) self.label_fade_length.setText( helpers.ms_to_mmss(self.current_track.fade_length)) - self.label_start_time.setText( - self.current_track.start_time.strftime( - Config.TRACK_TIME_FORMAT)) - self.label_end_time.setText( - self.current_track.end_time.strftime(Config.TRACK_TIME_FORMAT)) + if self.current_track.start_time: + self.label_start_time.setText( + self.current_track.start_time.strftime( + Config.TRACK_TIME_FORMAT)) + if self.current_track.end_time: + self.label_end_time.setText( + self.current_track.end_time.strftime( + Config.TRACK_TIME_FORMAT)) def resume(self) -> None: """ @@ -1157,6 +1232,8 @@ class Window(QMainWindow, Ui_MainWindow): # Reset next track if there was one if original_next_plr_id: next_plr = session.get(PlaylistRows, original_next_plr_id) + if not next_plr or not original_next_plr_playlist_tab: + return self.this_is_the_next_playlist_row( session, next_plr, original_next_plr_playlist_tab) @@ -1308,12 +1385,13 @@ class Window(QMainWindow, Ui_MainWindow): self.music.stop() # Reset playlist_tab colour - if self.current_track.playlist_tab == self.next_track.playlist_tab: - self.set_tab_colour(self.current_track.playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) - else: - self.set_tab_colour(self.current_track.playlist_tab, - QColor(Config.COLOUR_NORMAL_TAB)) + if self.current_track.playlist_tab: + if self.current_track.playlist_tab == self.next_track.playlist_tab: + self.set_tab_colour(self.current_track.playlist_tab, + QColor(Config.COLOUR_NEXT_TAB)) + else: + self.set_tab_colour(self.current_track.playlist_tab, + QColor(Config.COLOUR_NORMAL_TAB)) # Run end-of-track actions self.end_of_track_actions() @@ -1366,10 +1444,11 @@ class Window(QMainWindow, Ui_MainWindow): self.next_track = PlaylistTrack() self.next_track.set_plr(session, plr, playlist_tab) - self.next_track.playlist_tab.update_display(session) - if self.current_track.playlist_tab != self.next_track.playlist_tab: - self.set_tab_colour(self.next_track.playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) + if self.next_track.playlist_tab: + self.next_track.playlist_tab.update_display(session) + if self.current_track.playlist_tab != self.next_track.playlist_tab: + self.set_tab_colour(self.next_track.playlist_tab, + QColor(Config.COLOUR_NEXT_TAB)) # If we've changed playlist tabs for next track, refresh old one # to remove highligting of next track @@ -1505,14 +1584,14 @@ class Window(QMainWindow, Ui_MainWindow): class CartDialog(QDialog): """Edit cart details""" - def __init__(self, parent: QMainWindow, session: scoped_session, - cart: Carts) -> None: + def __init__(self, musicmuster: Window, session: scoped_session, + cart: Carts, *args, **kwargs) -> None: """ Manage carts """ - super().__init__(parent) - self.parent = parent + super().__init__(*args, **kwargs) + self.musicmuster = musicmuster self.session = session self.ui = Ui_DialogCartEdit() @@ -1522,7 +1601,7 @@ class CartDialog(QDialog): self.ui.lineEditName.setText(cart.name) self.ui.chkEnabled.setChecked(cart.enabled) - self.ui.windowTitle = "Edit Cart " + str(cart.id) + self.setWindowTitle("Edit Cart " + str(cart.id)) self.ui.btnFile.clicked.connect(self.choose_file) @@ -1542,8 +1621,8 @@ class CartDialog(QDialog): class DbDialog(QDialog): """Select track from database""" - def __init__(self, parent: Window, session: scoped_session, - get_one_track: bool = False) -> None: + def __init__(self, musicmuster: Window, session: scoped_session, + get_one_track: bool = False, *args, **kwargs) -> None: """ Subclassed QDialog to manage track selection @@ -1552,7 +1631,8 @@ class DbDialog(QDialog): to be added to the playlist. """ - super().__init__(parent) + super().__init__(*args, **kwargs) + self.musicmuster = musicmuster self.session = session self.get_one_track = get_one_track self.ui = Ui_Dialog() @@ -1564,6 +1644,7 @@ class DbDialog(QDialog): self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.searchString.textEdited.connect(self.chars_typed) + self.track: Optional[Tracks] = None if get_one_track: self.ui.txtNote.hide() @@ -1609,19 +1690,19 @@ class DbDialog(QDialog): """Add passed track to playlist on screen""" if self.get_one_track: - self.ui.track = track + self.track = track self.accept() return if track: - self.parent().visible_playlist_tab().insert_track( + self.musicmuster.visible_playlist_tab().insert_track( self.session, track, note=self.ui.txtNote.text()) else: - self.parent().visible_playlist_tab().insert_header( + self.musicmuster.visible_playlist_tab().insert_header( self.session, note=self.ui.txtNote.text()) # Save to database (which will also commit changes) - self.parent().visible_playlist_tab().save_playlist(self.session) + self.musicmuster.visible_playlist_tab().save_playlist(self.session) # Clear note field and select search text to make it easier for # next search self.ui.txtNote.clear() @@ -1684,7 +1765,7 @@ class DbDialog(QDialog): class DownloadCSV(QDialog): def __init__(self, parent=None): - super().__init__(parent) + super().__init__(*args, **kwargs) self.ui = Ui_DateSelect() self.ui.setupUi(self) @@ -1696,7 +1777,7 @@ class DownloadCSV(QDialog): class SelectPlaylistDialog(QDialog): def __init__(self, parent=None, playlists=None, session=None): - super().__init__(parent) + super().__init__(*args, **kwargs) if playlists is None: return diff --git a/app/playlists.py b/app/playlists.py index 57f2ae5..671d241 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -5,7 +5,7 @@ import threading from collections import namedtuple from datetime import datetime, timedelta -from typing import cast, List, Optional +from typing import cast, List, Optional, TYPE_CHECKING from PyQt5.QtCore import ( pyqtSignal, @@ -59,6 +59,9 @@ from models import ( NoteColours ) +if TYPE_CHECKING: + from musicmuster import Window + start_time_re = re.compile(r"@\d\d:\d\d:\d\d") HEADER_NOTES_COLUMN = 2 MINIMUM_ROW_HEIGHT = 30 @@ -129,10 +132,11 @@ class PlaylistTab(QTableWidget): ROW_DURATION = Qt.UserRole + 2 PLAYLISTROW_ID = Qt.UserRole + 3 - def __init__(self, musicmuster: QMainWindow, session: scoped_session, + def __init__(self, musicmuster: Window, + session: scoped_session, playlist_id: int, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self.musicmuster = musicmuster + self.musicmuster: Window = musicmuster self.playlist_id = playlist_id self.menu: Optional[QMenu] = None @@ -154,7 +158,7 @@ class PlaylistTab(QTableWidget): # Header row for idx in [a for a in range(len(columns))]: - item: QTableWidgetItem = QTableWidgetItem() + item = QTableWidgetItem() self.setHorizontalHeaderItem(idx, item) self.horizontalHeader().setMinimumSectionSize(0) # Set column headings sorted by idx @@ -183,7 +187,7 @@ class PlaylistTab(QTableWidget): self.itemSelectionChanged.connect(self._select_event) self.search_text: str = "" - self.edit_cell_type = None + self.edit_cell_type: Optional[int] self.selecting_in_progress = False # Connect signals self.horizontalHeader().sectionResized.connect(self._column_resize) @@ -212,7 +216,9 @@ class PlaylistTab(QTableWidget): rows: List = sorted(set(item.row() for item in self.selectedItems())) rows_to_move = [ - [QTableWidgetItem(self.item(row_index, column_index)) for + [QTableWidgetItem( + self.item(row_index, column_index) # type: ignore + ) for column_index in range(self.columnCount())] for row_index in rows ] @@ -407,10 +413,14 @@ class PlaylistTab(QTableWidget): # change cell again (metadata) self.cellChanged.disconnect(self._cell_changed) - new_text = self.item(row, column).text().strip() + cell = self.item(row, column) + if not cell: + return + + new_text = cell.text().strip() # Update cell with strip()'d text - self.item(row, column).setText(new_text) + cell.setText(new_text) track_id = self._get_row_track_id(row) @@ -419,6 +429,8 @@ class PlaylistTab(QTableWidget): # Get playlistrow object plr_id = self._get_playlistrow_id(row) plr_item = session.get(PlaylistRows, plr_id) + if not plr_item: + return # Note any updates needed to PlaylistTrack objects update_current = self.musicmuster.current_track.plr_id == plr_id @@ -473,7 +485,7 @@ class PlaylistTab(QTableWidget): super(PlaylistTab, self).closeEditor(editor, hint) - def edit(self, index: QModelIndex, + def edit(self, index: QModelIndex, # type: ignore # FIXME trigger: QAbstractItemView.EditTrigger, event: QEvent) -> bool: """ @@ -492,7 +504,6 @@ class PlaylistTab(QTableWidget): if track_row: # If a track row, we only allow editing of title, artist and # note. Check that this column is one of those. - self.edit_cell_type = None if column in [TITLE, ARTIST, ROW_NOTES]: self.edit_cell_type = column else: @@ -523,6 +534,8 @@ class PlaylistTab(QTableWidget): plr_id = self._get_playlistrow_id(row) plr_item = session.get(PlaylistRows, plr_id) item = self.item(row, note_column) + if not plr_item or not plr_item.note or not item: + return False item.setText(plr_item.note) # Connect signal so we know when cell has changed. @@ -563,6 +576,8 @@ class PlaylistTab(QTableWidget): """ plr_ids = self.get_selected_playlistrow_ids() + if not plr_ids: + return None return [session.get(PlaylistRows, a) for a in plr_ids] def insert_header(self, session: scoped_session, note: str, @@ -588,6 +603,9 @@ class PlaylistTab(QTableWidget): Insert passed playlist row (plr) into playlist tab. """ + if plr.row_number is None: + return + row = plr.row_number self.insertRow(row) @@ -604,44 +622,46 @@ class PlaylistTab(QTableWidget): start_gap = plr.track.start_gap except AttributeError: return - start_gap_item = QTableWidgetItem(str(start_gap)) + start_gap_item = self._set_item_text( + row, START_GAP, str(start_gap)) if start_gap and start_gap >= 500: start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) - self.setItem(row, START_GAP, start_gap_item) - title_item = QTableWidgetItem(plr.track.title) - self.setItem(row, TITLE, title_item) + track_title = plr.track.title + if not track_title: + track_title = "" + _ = self._set_item_text(row, TITLE, track_title) - artist_item = QTableWidgetItem(plr.track.artist) - self.setItem(row, ARTIST, artist_item) + track_artist = plr.track.artist + if not track_artist: + track_artist = "" + _ = self._set_item_text(row, ARTIST, track_artist) - duration_item = QTableWidgetItem( - ms_to_mmss(plr.track.duration)) - self.setItem(row, DURATION, duration_item) - self._set_row_duration(row, plr.track.duration) + _ = self._set_item_text(row, DURATION, + ms_to_mmss(plr.track.duration)) + if plr.track.duration: + self._set_row_duration(row, plr.track.duration) - start_item = QTableWidgetItem() - self.setItem(row, START_TIME, start_item) + _ = self._set_item_text(row, START_TIME, "") - end_item = QTableWidgetItem() - self.setItem(row, END_TIME, end_item) + _ = self._set_item_text(row, END_TIME, "") if plr.track.bitrate: bitrate = str(plr.track.bitrate) else: bitrate = "" - bitrate_item = QTableWidgetItem(bitrate) - self.setItem(row, BITRATE, bitrate_item) + _ = self._set_item_text(row, BITRATE, bitrate) # As we have track info, any notes should be contained in # the notes column - notes_item = QTableWidgetItem(plr.note) - self.setItem(row, ROW_NOTES, notes_item) + plr_note = plr.note + if not plr_note: + plr_note = "" + _ = self._set_item_text(row, ROW_NOTES, plr_note) last_playtime = Playdates.last_played(session, plr.track.id) last_played_str = get_relative_date(last_playtime) - last_played_item = QTableWidgetItem(last_played_str) - self.setItem(row, LASTPLAYED, last_played_item) + _ = self._set_item_text(row, LASTPLAYED, last_played_str) else: # This is a section header so it must have note text @@ -661,8 +681,7 @@ class PlaylistTab(QTableWidget): continue self.setItem(row, i, QTableWidgetItem()) self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) - notes_item = QTableWidgetItem(plr.note) - self.setItem(row, HEADER_NOTES_COLUMN, notes_item) + _ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note) # Save (no) track_id userdata_item.setData(self.ROW_TRACK_ID, 0) @@ -773,8 +792,9 @@ class PlaylistTab(QTableWidget): # Scroll to top if scroll_to_top: - scroll_to: QTableWidgetItem = self.item(0, 0) - self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) + row0_item = self.item(0, 0) + if row0_item: + self.scrollToItem(row0_item, QAbstractItemView.PositionAtTop) # Set widths self._set_column_widths(session) @@ -818,8 +838,8 @@ class PlaylistTab(QTableWidget): # Now build a dictionary of # {display_row_number: display_row_plr} - plr_dict_by_id = PlaylistRows.indexed_by_id(session, - display_plr_ids.values()) + plr_dict_by_id = PlaylistRows.indexed_by_id( + session, iter(display_plr_ids.values())) # type: ignore # FIXME # Finally a dictionary of # {display_row_number: plr} @@ -835,21 +855,24 @@ class PlaylistTab(QTableWidget): # that's not in the displayed playlist need to be deleted. # Ensure changes flushed - session.commit() - PlaylistRows.delete_plrids_not_in_list(session, self.playlist_id, - display_plr_ids.values()) + session.flush() + PlaylistRows.delete_plrids_not_in_list( + session, self.playlist_id, + iter(display_plr_ids.values())) # type: ignore # FIXME def scroll_current_to_top(self) -> None: """Scroll currently-playing row to top""" current_row = self._get_current_track_row_number() - self._scroll_to_top(current_row) + if current_row is not None: + self._scroll_to_top(current_row) def scroll_next_to_top(self) -> None: """Scroll nextly-playing row to top""" next_row = self._get_next_track_row_number() - self._scroll_to_top(next_row) + if next_row is not None: + self._scroll_to_top(next_row) def set_search(self, text: str) -> None: """Set search text and find first match""" @@ -1003,9 +1026,15 @@ class PlaylistTab(QTableWidget): # Extract note text from database to ignore section timings playlist_row = session.get(PlaylistRows, self._get_playlistrow_id(row)) + if not playlist_row: + continue note_text = playlist_row.note + if not note_text: + note_text = "" # Get note colour - note_colour = NoteColours.get_colour(session, note_text) + note_colour = None + if note_text: + note_colour = NoteColours.get_colour(session, note_text) # Get track if there is one track_id = self._get_row_track_id(row) @@ -1022,9 +1051,9 @@ class PlaylistTab(QTableWidget): else: note_text = f"track_id {missing_track} not found" playlist_row.note = note_text - session.commit() - note_item = QTableWidgetItem(note_text) - self.setItem(row, HEADER_NOTES_COLUMN, note_item) + session.flush() + _ = self._set_item_text(row, HEADER_NOTES_COLUMN, + note_text) if track: # Reset colour in case it was current/next/unplayable @@ -1042,33 +1071,36 @@ class PlaylistTab(QTableWidget): # Colour any note if note_colour: - (self.item(row, ROW_NOTES) - .setBackground(QColor(note_colour))) + notes_item = self.item(row, ROW_NOTES) + if notes_item: + notes_item.setBackground(QColor(note_colour)) # Highlight low bitrates if track.bitrate: bitrate_str = str(track.bitrate) - bitrate_item = self.item(row, BITRATE) - if bitrate_item.text() != bitrate_str: - bitrate_item.setText(bitrate_str) - if track.bitrate < Config.BITRATE_LOW_THRESHOLD: - cell_colour = Config.COLOUR_BITRATE_LOW - elif track.bitrate < Config.BITRATE_OK_THRESHOLD: - cell_colour = Config.COLOUR_BITRATE_MEDIUM - else: - cell_colour = Config.COLOUR_BITRATE_OK - brush = QBrush(QColor(cell_colour)) - self.item(row, BITRATE).setBackground(brush) + bitrate_item = self._set_item_text( + row, BITRATE, str(track.bitrate)) + if bitrate_item: + if track.bitrate < Config.BITRATE_LOW_THRESHOLD: + cell_colour = Config.COLOUR_BITRATE_LOW + elif track.bitrate < Config.BITRATE_OK_THRESHOLD: + cell_colour = Config.COLOUR_BITRATE_MEDIUM + else: + cell_colour = Config.COLOUR_BITRATE_OK + brush = QBrush(QColor(cell_colour)) + bitrate_item.setBackground(brush) # Render playing track if row == current_row: # Set last played time to "Today" - self.item(row, LASTPLAYED).setText("Today") + self._set_item_text( + row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING) # Calculate next_start_time - next_start_time = self._calculate_end_time( - self.musicmuster.current_track.start_time, - track.duration - ) + if track.duration: + next_start_time = self._calculate_end_time( + self.musicmuster.current_track.start_time, + track.duration + ) # Set end time self._set_row_end_time(row, next_start_time) # Set colour @@ -1093,8 +1125,9 @@ class PlaylistTab(QTableWidget): start_time = next_start_time self._set_row_start_time(row, start_time) # Calculate next_start_time - next_start_time = self._calculate_end_time(start_time, - track.duration) + if track.duration: + next_start_time = self._calculate_end_time( + start_time, track.duration) # Set end time self._set_row_end_time(row, next_start_time) # Set colour @@ -1106,8 +1139,8 @@ class PlaylistTab(QTableWidget): if row in played: # Played today, so update last played column - self.item(row, LASTPLAYED).setText( - Config.LAST_PLAYED_TODAY_STRING) + self._set_item_text( + row, LASTPLAYED, Config.LAST_PLAYED_TODAY_STRING) if self.musicmuster.hide_played_tracks: self.hideRow(row) else: @@ -1117,8 +1150,9 @@ class PlaylistTab(QTableWidget): # Set start/end times as we haven't played it yet if next_start_time: self._set_row_start_time(row, next_start_time) - next_start_time = self._calculate_end_time( - next_start_time, track.duration) + if track.duration: + next_start_time = self._calculate_end_time( + start_time, track.duration) # Set end time self._set_row_end_time(row, next_start_time) else: @@ -1176,28 +1210,34 @@ class PlaylistTab(QTableWidget): # Add track to playlist row plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + if not plr: + return + plr.track_id = track.id - session.commit() + session.flush() # Reset row span for column in range(len(columns)): self.setSpan(row, column, 1, 1) # Update attributes of row - self.item(row, USERDATA).setData(self.ROW_TRACK_ID, track.id) - start_gap_item = self.item(row, START_GAP) - start_gap_item.setText(str(track.start_gap)) - if track.start_gap and track.start_gap >= 500: - start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) - self.item(row, TITLE).setText(str(track.title)) - self.item(row, ARTIST).setText(str(track.artist)) - self.item(row, DURATION).setText(ms_to_mmss(track.duration)) + userdata_item = self.item(row, USERDATA) + if not userdata_item: + userdata_item = QTableWidgetItem() + userdata_item.setData(self.ROW_TRACK_ID, track.id) + last_playtime = Playdates.last_played(session, track.id) last_played_str = get_relative_date(last_playtime) - self.item(row, LASTPLAYED).setText(last_played_str) - self.item(row, ROW_NOTES).setText(plr.note) + _ = self._set_item_text(row, LASTPLAYED, last_played_str) - self.update_display(session) + _ = self._set_item_text(row, ROW_NOTES, plr.note) + + start_gap_item = self._set_item_text(row, START_GAP, + track.start_gap) + if track.start_gap and track.start_gap >= 500: + start_gap_item.setBackground(QColor(Config.COLOUR_LONG_START)) + + self._update_row(session, row, track) def _calculate_end_time(self, start: Optional[datetime], duration: int) -> Optional[datetime]: @@ -1246,7 +1286,7 @@ class PlaylistTab(QTableWidget): with Session() as session: track = session.get(Tracks, track_id) - if track: + if track and track.path: # Escape single quotes and spaces in name path = track.path pathq = path.replace("'", "\\'") @@ -1266,6 +1306,8 @@ class PlaylistTab(QTableWidget): # Delete rows from database plr_ids = self.get_selected_playlistrow_ids() + if not plr_ids: + return # Get confirmation row_count = len(plr_ids) @@ -1292,7 +1334,8 @@ class PlaylistTab(QTableWidget): else index.row()) def _find_next_track_row(self, session: scoped_session, - starting_row: int = None) -> Optional[int]: + starting_row: Optional[int] = None) \ + -> Optional[int]: """ Find next track to play. If a starting row is given, start there; otherwise, start from top. Skip rows already played. @@ -1315,6 +1358,8 @@ class PlaylistTab(QTableWidget): ] for row in range(starting_row, self.rowCount()): plr = self._get_playlistrow_object(session, row) + if not plr: + continue if ( row not in track_rows or row in played_rows or @@ -1330,12 +1375,18 @@ class PlaylistTab(QTableWidget): """Return current track row or None""" current_track = self.musicmuster.current_track + if not current_track or not current_track.plr_id: + return None + return self._plrid_to_row_number(current_track.plr_id) def _get_next_track_row_number(self) -> Optional[int]: """Return next track row or None""" next_track = self.musicmuster.next_track + if not next_track or not next_track.plr_id: + return None + return self._plrid_to_row_number(next_track.plr_id) @staticmethod @@ -1355,18 +1406,27 @@ class PlaylistTab(QTableWidget): except ValueError: return None - def _get_playlistrow_id(self, row: int) -> int: + def _get_playlistrow_id(self, row: int) -> Optional[int]: """Return the playlistrow_id associated with this row""" - playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID)) + userdata_item = self.item(row, USERDATA) + if not userdata_item: + return None - return playlistrow_id + return userdata_item.data(self.PLAYLISTROW_ID) def _get_playlistrow_object(self, session: scoped_session, - row: int) -> PlaylistRows: + row: int) -> Optional[PlaylistRows]: """Return the playlistrow object associated with this row""" - playlistrow_id = (self.item(row, USERDATA).data(self.PLAYLISTROW_ID)) + userdata_item = self.item(row, USERDATA) + if not userdata_item: + return None + + playlistrow_id = userdata_item.data(self.PLAYLISTROW_ID) + if not playlistrow_id: + return None + return session.get(PlaylistRows, playlistrow_id) def _get_row_artist(self, row: int) -> Optional[str]: @@ -1377,12 +1437,19 @@ class PlaylistTab(QTableWidget): return None item_artist = self.item(row, ARTIST) + if not item_artist: + return None + return item_artist.text() def _get_row_duration(self, row: int) -> int: """Return duration associated with this row""" - duration = (self.item(row, USERDATA).data(self.ROW_DURATION)) + userdata_item = self.item(row, USERDATA) + if not userdata_item: + return 0 + + duration = userdata_item.data(self.ROW_DURATION) if duration: return duration else: @@ -1396,17 +1463,21 @@ class PlaylistTab(QTableWidget): item_note = self.item(row, ROW_NOTES) else: item_note = self.item(row, HEADER_NOTES_COLUMN) + if not item_note: + return None + return item_note.text() def _get_row_start_time(self, row: int) -> Optional[datetime]: + """Return row start time as string or None""" + + start_time_item = self.item(row, START_TIME) + if not start_time_item: + return None + try: - if self.item(row, START_TIME): - return datetime.strptime(self.item( - row, START_TIME).text(), - Config.NOTE_TIME_FORMAT - ) - else: - return None + return datetime.strptime(start_time_item.text(), + Config.NOTE_TIME_FORMAT) except ValueError: return None @@ -1418,16 +1489,22 @@ class PlaylistTab(QTableWidget): return None item_title = self.item(row, TITLE) + if not item_title: + return None + return item_title.text() def _get_row_track_id(self, row: int) -> int: """Return the track_id associated with this row or None""" + userdata_item = self.item(row, USERDATA) + if not userdata_item: + return 0 + try: - track_id = (self.item(row, USERDATA) - .data(self.ROW_TRACK_ID)) + track_id = userdata_item.data(self.ROW_TRACK_ID) except AttributeError: - return None + return 0 return track_id @@ -1496,6 +1573,9 @@ class PlaylistTab(QTableWidget): new_row_number: int) -> None: """Move playlist row to new_row_number using parent copy/paste""" + if plr.row_number is None: + return + # Remove source row self.removeRow(plr.row_number) # Fixup plr row number @@ -1535,7 +1615,13 @@ class PlaylistTab(QTableWidget): ) return - open_in_audacity(track.path) + if track.path is None: + log.error( + f"playlists._open_in_audacity({track_id=}): " + "Track has no path" + ) + else: + open_in_audacity(track.path) def _plrid_to_row_number(self, plrid: int) -> Optional[int]: """ @@ -1559,6 +1645,9 @@ class PlaylistTab(QTableWidget): # Update playlist_rows record with Session() as session: plr = session.get(PlaylistRows, self._get_playlistrow_id(row)) + if not plr: + return + plr.track_id = None # We can't have null text if not plr.note: @@ -1567,15 +1656,17 @@ class PlaylistTab(QTableWidget): # Clear track text items for i in range(2, len(columns)): - self.item(row, i).setText("") + _ = self._set_item_text(row, i, "") # Remove row duration self._set_row_duration(row, 0) # Remote track_id from row - self.item(row, USERDATA).setData(self.ROW_TRACK_ID, 0) + userdata_item = self.item(row, USERDATA) + if userdata_item: + userdata_item.setData(self.ROW_TRACK_ID, 0) # Span the rows self.setSpan(row, HEADER_NOTES_COLUMN, 1, len(columns) - 1) # Set note text in correct column for section head - self.item(row, HEADER_NOTES_COLUMN).setText(plr.note) + _ = self._set_item_text(row, HEADER_NOTES_COLUMN, plr.note) # And refresh display self.update_display(session) @@ -1730,6 +1821,19 @@ class PlaylistTab(QTableWidget): else: self.setColumnWidth(idx, Config.DEFAULT_COLUMN_WIDTH) + def _set_item_text(self, row, column, text) -> QTableWidgetItem: + """ + Set text for item if it exists, else create it, and return item + """ + + item = self.item(row, column) + if not item: + item = QTableWidgetItem(text) + self.setItem(row, column, item) + else: + item.setText(text) + return item + def _set_next(self, session: scoped_session, row_number: int) -> None: """ Set passed row as next playlist row to play. @@ -1759,7 +1863,10 @@ class PlaylistTab(QTableWidget): # Notify musicmuster plr = session.get(PlaylistRows, self._get_playlistrow_id(row_number)) - self.musicmuster.this_is_the_next_playlist_row(session, plr, self) + if not plr: + log.debug(f"playists._set_next({row_number=}) can't retrieve plr") + else: + self.musicmuster.this_is_the_next_playlist_row(session, plr, self) # Update display self.clear_selection() @@ -1787,8 +1894,9 @@ class PlaylistTab(QTableWidget): for column in range(self.columnCount()): if column == ROW_NOTES: continue - if self.item(row, column): - self.item(row, column).setFont(boldfont) + item = self.item(row, column) + if item: + item.setFont(boldfont) def _set_row_colour(self, row: int, colour: Optional[QColor] = None) -> None: @@ -1807,21 +1915,25 @@ class PlaylistTab(QTableWidget): # Don't change colour on start gap columns if column == START_GAP: continue - if self.item(row, column): - self.item(row, column).setBackground(brush) + item = self.item(row, column) + if item: + item.setBackground(brush) def _set_row_duration(self, row: int, ms: int) -> None: """Set duration of this row in row metadata""" - self.item(row, USERDATA).setData(self.ROW_DURATION, ms) + item = self.item(row, USERDATA) + if item: + item.setData(self.ROW_DURATION, ms) def _set_row_end_time(self, row: int, time: Optional[datetime]) -> None: """Set passed row end time to passed time""" try: - time_str = time.strftime(Config.TRACK_TIME_FORMAT) + time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore except AttributeError: time_str = "" + item = QTableWidgetItem(time_str) self.setItem(row, END_TIME, item) @@ -1834,14 +1946,13 @@ class PlaylistTab(QTableWidget): """Set passed row start time to passed time""" try: - time_str = time.strftime(Config.TRACK_TIME_FORMAT) + time_str = time.strftime(Config.TRACK_TIME_FORMAT) # type: ignore except AttributeError: time_str = "" - item = QTableWidgetItem(time_str) - self.setItem(row, START_TIME, item) + _ = self._set_item_text(row, START_TIME, time_str) def _get_section_timing_string(self, ms: int, - no_end: bool = False) -> None: + no_end: bool = False) -> str: """Return string describing section duration""" duration = ms_to_mmss(ms) diff --git a/app/replace_files.py b/app/replace_files.py index 679b54c..01a4456 100755 --- a/app/replace_files.py +++ b/app/replace_files.py @@ -36,7 +36,6 @@ parent_dir = os.path.dirname(source_dir) name_and_tags: List[str] = [] tags_not_name: List[str] = [] # multiple_similar: List[str] = [] -no_match: List[str] = [] # possibles: List[str] = [] no_match: int = 0 diff --git a/pyproject.toml b/pyproject.toml index 128498b..9c67525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,8 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.mypy] -mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app" +# mypy_path = "/home/kae/.cache/pypoetry/virtualenvs/musicmuster-oWgGw1IG-py3.9:/home/kae/git/musicmuster/app" +mypy_path = "/home/kae/git/musicmuster/app" plugins = "sqlalchemy.ext.mypy.plugin" [tool.vulture]