diff --git a/app/config.py b/app/config.py index a4bc57d..55bd173 100644 --- a/app/config.py +++ b/app/config.py @@ -5,6 +5,7 @@ import os class Config(object): AUDACITY_COMMAND = "/usr/bin/audacity" AUDIO_SEGMENT_CHUNK_SIZE = 10 + CHECK_AUDACITY_AT_STARTUP = True COLOUR_CURRENT_HEADER = "#d4edda" COLOUR_CURRENT_PLAYLIST = "#7eca8f" COLOUR_CURRENT_TAB = "#248f24" @@ -50,6 +51,7 @@ class Config(object): MILLISECOND_SIGFIGS = 0 MYSQL_CONNECT = os.environ.get('MYSQL_CONNECT') or "mysql+mysqldb://musicmuster:musicmuster@localhost/musicmuster_v2" # noqa E501 NORMALISE_ON_IMPORT = True + NOTE_TIME_FORMAT = "%H:%M:%S" ROOT = os.environ.get('ROOT') or "/home/kae/music" TESTMODE = True TIMER_MS = 500 diff --git a/app/helpers.py b/app/helpers.py index 6c54e3f..d53535e 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -160,6 +160,10 @@ def open_in_audacity(path): if "audacity" not in [i.name() for i in psutil.process_iter()]: return False + # Return if path not given + if not path: + return False + to_pipe = '/tmp/audacity_script_pipe.to.' + str(os.getuid()) from_pipe = '/tmp/audacity_script_pipe.from.' + str(os.getuid()) EOL = '\n' diff --git a/app/models.py b/app/models.py index b30a20e..e316f66 100644 --- a/app/models.py +++ b/app/models.py @@ -6,8 +6,6 @@ import re import sqlalchemy from datetime import datetime -from mutagen.flac import FLAC -from mutagen.mp3 import MP3 from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declarative_base, DeclarativeMeta from sqlalchemy import ( @@ -30,7 +28,6 @@ from helpers import ( fade_point, get_audio_segment, leading_silence, - show_warning, trailing_silence, ) from log import DEBUG, ERROR diff --git a/app/musicmuster.py b/app/musicmuster.py index c497dff..10a2f39 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -36,23 +36,6 @@ from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist from ui.main_window_ui import Ui_MainWindow -class ElideLabel(QLabel): - """ - From https://stackoverflow.com/questions/11446478/ - pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize - """ - - def paintEvent(self, event): - #TODO: V2 check - painter = QPainter(self) - - metrics = QFontMetrics(self.font()) - elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) - - painter.drawText(self.rect(), self.alignment(), elided) - #TODO: V2 check - - class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): #TODO: V2 check @@ -120,8 +103,10 @@ class Window(QMainWindow, Ui_MainWindow): self.setGeometry(x, y, width, height) def check_audacity(self): - #TODO: V2 check - "Offer to run Audacity if not running" + """Offer to run Audacity if not running""" + + if not Config.CHECK_AUDACITY_AT_STARTUP: + return if "audacity" in [i.name() for i in psutil.process_iter()]: return @@ -427,15 +412,16 @@ class Window(QMainWindow, Ui_MainWindow): for playlist_db in Playlists.get_open(session): self.create_playlist_tab(session, playlist_db) - def create_playlist_tab(self, session, playlist_db): + def create_playlist_tab(self, session, playlist): #TODO: V2 check """ - Take the passed database object, create a playlist tab and add tab - to display. + Take the passed playlist database object, create a playlist tab and + add tab to display. """ - playlist_tab = PlaylistTab(self, session, playlist_db) - idx = self.tabPlaylist.addTab(playlist_tab, playlist_db.name) + playlist_tab = PlaylistTab(parent=self, + session=session, playlist=playlist) + idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) self.tabPlaylist.setCurrentIndex(idx) def move_selected(self): @@ -466,11 +452,10 @@ class Window(QMainWindow, Ui_MainWindow): break rows = [] - for (row, track_id) in ( + for (row, track) in ( self.visible_playlist_tab().get_selected_rows_and_tracks() ): rows.append(row) - track = Tracks.get_by_id(session, track_id) if destination_visible_playlist_tab: # Insert with repaint=False to not update database destination_visible_playlist_tab.insert_track( @@ -538,7 +523,7 @@ class Window(QMainWindow, Ui_MainWindow): # no automatic next track, and may later be overriden by # user selecting a different track on this or another # playlist. - next_track_id = self.current_track_playlist_tab.play_started() + ***KAE won't return next_track_id *** = self.current_track_playlist_tab.play_started() if next_track_id is not None: self.next_track = Tracks.get_by_id(session, next_track_id) @@ -1019,6 +1004,5 @@ def main(): EXCEPTION("Unhandled Exception caught by musicmuster.main()") -print(f"{__name__=}") if __name__ == "__main__": main() diff --git a/app/playlists.py b/app/playlists.py index 49df141..5395b07 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -1,3 +1,5 @@ +from typing import Optional + from PyQt5 import QtCore from PyQt5.QtCore import Qt from PyQt5.Qt import QFont @@ -23,17 +25,23 @@ from log import DEBUG, ERROR from models import ( Notes, Playdates, - Playlists, Session, Settings, Tracks, NoteColours ) -from utilities import create_track_from_file + + +class RowMeta: + CLEAR = 0 + NOTE = 1 + UNREADABLE = 2 + NEXT = 4 + CURRENT = 8 + PLAYED = 16 class PlaylistTab(QTableWidget): - cellEditingStarted = QtCore.pyqtSignal(int, int) cellEditingEnded = QtCore.pyqtSignal() @@ -55,12 +63,13 @@ class PlaylistTab(QTableWidget): ROW_METADATA = Qt.UserRole CONTENT_OBJECT = Qt.UserRole + 1 - def __init__(self, parent, session, playlist_db, *args, **kwargs): + def __init__(self, parent, session, playlist, *args, **kwargs): super().__init__(*args, **kwargs) - self.master_process = self.parent() # The MusicMuster process - self.playlist = playlist_db + self.parent = parent # The MusicMuster process + self.playlist = playlist self.playlist.mark_open(session) + self.menu = None # Set up widget self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) @@ -106,7 +115,6 @@ class PlaylistTab(QTableWidget): self.viewport().setAcceptDrops(True) self.setDragDropOverwriteMode(False) self.setDropIndicatorShown(True) - self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) @@ -127,45 +135,46 @@ class PlaylistTab(QTableWidget): self.cellEditingStarted.connect(self._cell_edit_started) self.cellEditingEnded.connect(self._cell_edit_ended) + # Now load our tracks and notes self.populate(session) self.current_track_start_time = None - self.played_tracks = [] + + def __repr__(self): + return f"" # ########## Events ########## def dropEvent(self, event: QDropEvent): - # TODO: V2 check - if not event.isAccepted() and event.source() == self: - drop_row = self._drop_on(event) + # if not event.isAccepted() and event.source() == self: + if not event.source() == self: + return # We don't accept external drops - rows = sorted(set(item.row() for item in self.selectedItems())) - rows_to_move = [ - [QTableWidgetItem(self.item(row_index, column_index)) for - column_index in range(self.columnCount())] - for row_index in rows - ] - for row_index in reversed(rows): - self.removeRow(row_index) - if row_index < drop_row: - drop_row -= 1 + drop_row = self._drop_on(event) - for row_index, data in enumerate(rows_to_move): - row_index += drop_row - self.insertRow(row_index) - for column_index, column_data in enumerate(data): - self.setItem(row_index, column_index, column_data) - event.accept() - # We don't want rows to be selected after move - # for row_index in range(len(rows_to_move)): - # for column_index in range(self.columnCount()): - # self.item(drop_row + row_index, - # column_index).setSelected(True) + rows = sorted(set(item.row() for item in self.selectedItems())) + rows_to_move = [ + [QTableWidgetItem(self.item(row_index, column_index)) for + column_index in range(self.columnCount())] + for row_index in rows + ] + for row_index in reversed(rows): + self.removeRow(row_index) + if row_index < drop_row: + drop_row -= 1 + + for row_index, data in enumerate(rows_to_move): + row_index += drop_row + self.insertRow(row_index) + for column_index, column_data in enumerate(data): + self.setItem(row_index, column_index, column_data) + event.accept() # The above doesn't handle column spans, which we use in note # rows. Check and fix: + row = 0 # So row is defined even if there are no rows in range for row in range(drop_row, drop_row + len(rows_to_move)): if row in self._meta_get_notes(): - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - self.NOTE_COL_SPAN) + self.setSpan( + row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) # Scroll to drop zone self.scrollToItem(self.item(row, 1)) @@ -180,37 +189,34 @@ class PlaylistTab(QTableWidget): self.save_playlist(session) self.update_display() - def edit(self, index, trigger, event): - # TODO: V2 check - result = super(PlaylistTab, self).edit(index, trigger, event) + def edit(self, index): + result = super(PlaylistTab, self).edit(index) if result: self.cellEditingStarted.emit(index.row(), index.column()) return result def closeEditor(self, editor, hint): - # TODO: V2 check super(PlaylistTab, self).closeEditor(editor, hint) self.cellEditingEnded.emit() def eventFilter(self, source, event): - # TODO: V2 check - "Used to process context (right-click) menu" + """Used to process context (right-click) menu, which is defined here""" - if(event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504 - event.buttons() == QtCore.Qt.RightButton and # noqa W504 - source is self.viewport()): + if (event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504 + event.buttons() == QtCore.Qt.RightButton and # noqa W504 + source is self.viewport()): item = self.itemAt(event.pos()) if item is not None: row = item.row() DEBUG(f"playlist.eventFilter(): Right-click on row {row}") current = row == self._meta_get_current() - next = row == self._meta_get_next() + next_row = row == self._meta_get_next() self.menu = QMenu(self) act_info = self.menu.addAction('Info') act_info.triggered.connect(lambda: self._info_row(row)) self.menu.addSeparator() if row not in self._meta_get_notes(): - if not current and not next: + if not current and not next_row: act_setnext = self.menu.addAction("Set next") act_setnext.triggered.connect( lambda: self._set_next(row)) @@ -224,7 +230,7 @@ class PlaylistTab(QTableWidget): "Open track in Audacity") act_audacity.triggered.connect( lambda: self._audacity(row)) - if not current and not next: + if not current and not next_row: self.menu.addSeparator() act_delete = self.menu.addAction('Delete') act_delete.triggered.connect(self._delete_rows) @@ -233,34 +239,33 @@ class PlaylistTab(QTableWidget): # ########## Externally called functions ########## - def close(self, session): - # TODO: V2 check - "Save column widths" + def closeEvent(self, event): + """Save column widths""" - for column in range(self.columnCount()): - width = self.columnWidth(column) - name = f"playlist_col_{str(column)}_width" - record = Settings.get_int(session, name) - if record.f_int != self.columnWidth(column): - record.update(session, {'f_int': width}) + with Session() as session: + for column in range(self.columnCount()): + width = self.columnWidth(column) + name = f"playlist_col_{str(column)}_width" + record = Settings.get_int(session, name) + if record.f_int != self.columnWidth(column): + record.update(session, {'f_int': width}) + + event.accept() def clear_current(self): - # TODO: V2 check - "Clear current track" + """Clear current track""" self._meta_clear_current() self.update_display() def clear_next(self): - # TODO: V2 check """Clear next track""" self._meta_clear_next() self.update_display() def get_selected_row(self): - # TODO: V2 check - "Return row number of first selected row, or None if none selected" + """Return row number of first selected row, or None if none selected""" if not self.selectionModel().hasSelection(): return None @@ -268,8 +273,7 @@ class PlaylistTab(QTableWidget): return self.selectionModel().selectedRows()[0].row() def get_selected_rows_and_tracks(self): - # TODO: V2 check - "Return a list of selected (rows, track_id) tuples" + """Return a list of selected (rows, track) tuples""" if not self.selectionModel().hasSelection(): return None @@ -277,14 +281,12 @@ class PlaylistTab(QTableWidget): result = [] for row in [r.row() for r in self.selectionModel().selectedRows()]: - track_id = self._get_row_id(row) - result.append((row, track_id)) + result.append((row, self._get_row_object(row))) return result def get_selected_title(self): - # TODO: V2 check - "Return title of selected row or None" + """Return title of selected row or None""" if self.selectionModel().hasSelection(): row = self.currentRow() @@ -293,12 +295,11 @@ class PlaylistTab(QTableWidget): return None def remove_rows(self, rows): - # TODO: V2 check - "Remove rows passed in rows list" + """Remove rows passed in rows list""" # Row number will change as we delete rows. We could use - # QPersistentModelIndex, but easier just to remove them lowest - # row first + # QPersistentModelIndex, but easier just to remove them in + # reverse order. for row in sorted(rows, reverse=True): self.removeRow(row) @@ -309,43 +310,36 @@ class PlaylistTab(QTableWidget): self.update_display() def play_started(self): - # TODO: V2 check """ Update current track to be what was next, and determine next track. - Return next track_id. + Return None """ self.current_track_start_time = datetime.now() current_row = self._meta_get_next() self._meta_set_current(current_row) - self.played_tracks.append(self._get_row_id(current_row)) + self._meta_set_played(current_row) # Scroll to put current track in centre scroll_to = self.item(current_row, self.COL_INDEX) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter) - # Get next track, but skip tracks already played - search_starting_row = current_row + 1 - while True: - next_track_row = self._find_next_track_row(search_starting_row) - next_track_id = self._set_next(next_track_row) - if next_track_id not in self.played_tracks: - break - search_starting_row += 1 + # Set next track + search_from = current_row + 1 + next_row = self._find_next_track_row(search_from) + self._set_next(next_row) self.update_display() - return next_track_id def play_stopped(self): - # TODO: V2 check self._meta_clear_current() self.current_track_start_time = None self.update_display() def populate(self, session): """ - Populate ourself from the associated playlist object + Populate from the associated playlist object We don't mandate that an item will be on its specified row, only that it will be above larger-numbered row items, and below @@ -354,6 +348,12 @@ class PlaylistTab(QTableWidget): data = [] + # Make sure the database object is usable + insp = inspect(self.playlist) + if insp.detached: + session.add(self.playlist) + assert insp.persistent + for row, track in self.playlist.tracks.items(): data.append(([row], track)) for note in self.playlist.notes: @@ -374,13 +374,12 @@ class PlaylistTab(QTableWidget): scroll_to = self.item(0, self.COL_TITLE) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) - # We possibly don't need to save the playlist here, but a) row - # numbers may have changed during population, and b) it's cheap + # We possibly don't need to save the playlist here, but row + # numbers may have changed during population, and it's cheap to do self.save_playlist(session) self.update_display() def save_playlist(self, session): - # TODO: V2 check """ Save playlist to database. @@ -393,32 +392,8 @@ class PlaylistTab(QTableWidget): changes. """ - # TODO: do we need to add ourself to the session? - insp = inspect(self.playlist) - transient = insp.transient - pending = insp.pending - persistent = insp.persistent - deleted = insp.deleted - detached = insp.detached - if transient: - DEBUG("playlist is transient") - session.add(self.playlist) - elif pending: - DEBUG("playlist is pending") - elif persistent: - DEBUG("playlist is persistent") - elif deleted: - DEBUG("playlist is deleted") - elif detached: - DEBUG("playlist is detached") - session.add(self.playlist) - assert inspect(self.playlist) == pending - else: - DEBUG("Can't find state of playlist") - - # TODO: hopefully we don't need to do this: - # playlist = session.query(Playlists).filter( - # Playlists.id == TODO: self.id).one() + # Ensure we have a valid database class + session.add(self.playlist) # Notes first # Create dictionaries indexed by note_id @@ -428,7 +403,9 @@ class PlaylistTab(QTableWidget): # PlaylistTab for row in notes_rows: - playlist_notes[note.id] = self._get_row_content(row) + note = self._get_row_object(row) + session.add(note) + playlist_notes[note.id] = note # Database for note in self.playlist.notes: @@ -442,7 +419,7 @@ class PlaylistTab(QTableWidget): for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): DEBUG( "_save_playlist(): " - f"Delete {note_id=} from {playlist=} in database" + f"Delete {note_id=} from {self=} in database" ) database_notes[note_id].delete_note(session) @@ -464,10 +441,10 @@ class PlaylistTab(QTableWidget): for row in range(self.rowCount()): if row in notes_rows: continue - playlist.add_track(session, track, row) + track = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + self.playlist.add_track(session, track, row) def select_next_row(self): - # TODO: V2 check """ Select next or first row. Don't select notes. Wrap at last row. """ @@ -500,23 +477,11 @@ class PlaylistTab(QTableWidget): self.selectRow(row) def select_played_tracks(self): - # TODO: V2 check """Select all played tracks in playlist""" - # Need to allow multiple rows to be selected - self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - - self.clearSelection() - - for row in range(self.rowCount()): - if self._get_row_id(row) in self.played_tracks: - self.selectRow(row) - - # Reset extended selection - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self._select_tracks(played=True) def select_previous_row(self): - # TODO: V2 check """ Select previous or last track. Don't select notes. Wrap at first row. """ @@ -550,40 +515,23 @@ class PlaylistTab(QTableWidget): self.selectRow(row) def select_unplayed_tracks(self): - # TODO: V2 check - "Select all unplayed tracks in playlist" + """Select all unplayed tracks in playlist""" - # Need to allow multiple rows to be selected - self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - notes_rows = self._meta_get_notes() - - self.clearSelection() - - for row in range(self.rowCount()): - if row in notes_rows: - continue - if self._get_row_id(row) in self.played_tracks: - continue - self.selectRow(row) - - # Reset extended selection - self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) + self._select_tracks(played=False) def set_selected_as_next(self): - # TODO: V2 check """ Sets the selected track as the next track. """ - if not self.selectionModel().hasSelection(): + if len(self.selectedItems()) != 1: return self._set_next(self.currentRow()) self.update_display() def update_display(self, clear_selection=True): - # TODO: V2 check - "Set row colours, fonts, etc" + """Set row colours, fonts, etc""" DEBUG(f"playlist.update_display [{self.playlist=}]") @@ -591,9 +539,10 @@ class PlaylistTab(QTableWidget): if clear_selection: self.clearSelection() - current = self._meta_get_current() - next = self._meta_get_next() + current_row = self._meta_get_current() + next_row = self._meta_get_next() notes = self._meta_get_notes() + played = self._meta_get_played() unreadable = self._meta_get_unreadable() # Set colours and start times @@ -601,68 +550,35 @@ class PlaylistTab(QTableWidget): # Don't change start times for tracks that have been played. # For unplayed tracks, if there's a 'current' or 'next' - # track marked, populate start times from then onwards. If - # neither, populate start times from first note with a start - # time. - if current and next: - start_times_row = min(current, next) + # track marked, populate start times from then onwards. A note + # with a start time will reset the next track start time. + if current_row and next_row: + start_times_row = min(current_row, next_row) else: - start_times_row = current or next + start_times_row = current_row or next_row if not start_times_row: start_times_row = 0 # Cycle through all rows for row in range(self.rowCount()): # We can't calculate start times until next_start_time is - # set. That can be set by either a note with a time, or the - # current track. + # set. if row in notes: - # TODO: check whether note has a time - - # # Does note end with a time? - # start_time = None - # try: - # start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time() - # DEBUG( - # f"playlist.inset_note(): Note contains valid time={start_time}" - # ) - # except ValueError: - # DEBUG( - # f"playlist.inset_note(): Note on row {row} ('{note.note}') " - # "does not contain valid time" - # ) - - # self.insertRow(row) - - # item = QTableWidgetItem(note) - # self.setItem(row, self.COL_INDEX, item) - # titleitem = QTableWidgetItem(note.note) - # self.setItem(row, self.COL_NOTE, titleitem) - # self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - # self.NOTE_COL_SPAN) - - # # Add start/end times or empty items as background - # # colour won't be set for columns without items - # self._set_row_start_time(row, start_time) - # item = QTableWidgetItem() - # self.setItem(row, self.COL_END_TIME, item) - # item = QTableWidgetItem() - # self.setItem(row, self.COL_LAST_PLAYED, item) - - # self._meta_set_note(row) - - row_time = self._get_row_time(row) + # Extract note text + note_text = self.item(row, self.COL_TITLE).text() + # Does the note end with a time? + row_time = self._get_note_text_time(note_text) if row_time: next_start_time = row_time # Set colour - note_text = self.item(row, self.COL_TITLE).text() note_colour = NoteColours.get_colour(session, note_text) if not note_colour: note_colour = Config.COLOUR_NOTES_PLAYLIST self._set_row_colour( row, QColor(note_colour) ) + # Notes are always bold self._set_row_bold(row) elif row in unreadable: @@ -672,17 +588,20 @@ class PlaylistTab(QTableWidget): ) self._set_row_bold(row) - elif row == current: + elif row == current_row: + # Extract track object + track = self._get_row_object(row) # Set start time self._set_row_start_time( row, self.current_track_start_time) + # Set last played time last_played_str = get_relative_date( self.current_track_start_time) self.item(row, self.COL_LAST_PLAYED).setText( last_played_str) # Calculate next_start_time - next_start_time = self._calculate_next_start_time( - session, row, self.current_track_start_time) + next_start_time = self._calculate_track_end_time( + track, self.current_track_start_time) # Set end time self._set_row_end_time(row, next_start_time) # Set colour @@ -691,11 +610,13 @@ class PlaylistTab(QTableWidget): # Make bold self._set_row_bold(row) - elif row == next: + elif row == next_row: + # Extract track object + track = self._get_row_object(row) # if there's a track playing, set start time from that if self.current_track_start_time: - start_time = self._calculate_next_start_time( - session, current, self.current_track_start_time) + start_time = self._calculate_track_end_time( + track, self.current_track_start_time) else: # No current track to base from, but don't change # time if it's already set @@ -704,8 +625,8 @@ class PlaylistTab(QTableWidget): start_time = next_start_time # Now set it self._set_row_start_time(row, start_time) - next_start_time = self._calculate_next_start_time( - session, row, start_time) + next_start_time = self._calculate_track_end_time( + track, start_time) # Set end time self._set_row_end_time(row, next_start_time) # Set colour @@ -715,28 +636,22 @@ class PlaylistTab(QTableWidget): self._set_row_bold(row) else: - # Stripe remaining rows - if row % 2: - colour = QColor(Config.COLOUR_ODD_PLAYLIST) - else: - colour = QColor(Config.COLOUR_EVEN_PLAYLIST) - self._set_row_colour(row, colour) - - track_id = self._get_row_id(row) - if track_id in self.played_tracks: + # Extract track object + track = self._get_row_object(row) + if row in played: # Played today, so update last played column last_playtime = Playdates.last_played( - session, track_id) + session, track.id) last_played_str = get_relative_date(last_playtime) self.item(row, self.COL_LAST_PLAYED).setText( last_played_str) self._set_row_not_bold(row) else: - # Set start/end times only if we haven't played it yet + # Set start/end times as we haven't played it yet if next_start_time and row >= start_times_row: self._set_row_start_time(row, next_start_time) - next_start_time = self._calculate_next_start_time( - session, row, next_start_time) + next_start_time = self._calculate_track_end_time( + track, next_start_time) # Set end time self._set_row_end_time(row, next_start_time) else: @@ -745,47 +660,48 @@ class PlaylistTab(QTableWidget): self._set_row_end_time(row, None) # Don't dim unplayed tracks self._set_row_bold(row) + # Stripe rows + if row % 2: + self._set_row_colour( + row, QColor(Config.COLOUR_ODD_PLAYLIST)) + else: + self._set_row_colour( + row, QColor(Config.COLOUR_EVEN_PLAYLIST)) # ########## Internally called functions ########## def _audacity(self, row): - # TODO: V2 check - "Open track in Audacity. Audacity must be already running" + """Open track in Audacity. Audacity must be already running""" DEBUG(f"_audacity({row})") if row in self._meta_get_notes(): return None - track_id = self._get_row_id(row) - if track_id: - with Session() as session: - track = Tracks.get_by_id(session, track_id) - open_in_audacity(track.path) + track = self._get_row_object(row) + open_in_audacity(track.path) - def _calculate_next_start_time(self, session, row, start): - # TODO: V2 check - "Return this row's end time given its start time" + @staticmethod + def _calculate_track_end_time(track, start: datetime) -> Optional[datetime]: + """Return this track's end time given its start time""" if start is None: return None - if row is None: - DEBUG("_calculate_next_start_time() called with row=None") + if track is None: + DEBUG("_calculate_next_start_time() called with track=None") return None duration = Tracks.get_duration(session, self._get_row_id(row)) return start + timedelta(milliseconds=duration) def _context_menu(self, pos): - # TODO: V2 check self.menu.exec_(self.mapToGlobal(pos)) def _copy_path(self, row): - # TODO: V2 check """ If passed row is track row, copy the track path to the clipboard. - Otherwise return None. + Otherwise, return None. """ DEBUG(f"_copy_path({row})") @@ -793,82 +709,73 @@ class PlaylistTab(QTableWidget): if row in self._meta_get_notes(): return None - track_id = self._get_row_id(row) - if track_id: - with Session() as session: - path = Tracks.get_path(session, track_id) + track = self._get_row_object(row) + if track: cb = QApplication.clipboard() cb.clear(mode=cb.Clipboard) - cb.setText(path, mode=cb.Clipboard) + cb.setText(track.path, mode=cb.Clipboard) def _cell_changed(self, row, column): - # TODO: V2 check - "Called when cell content has changed" + """Called when cell content has changed""" if not self.editing_cell: return - # If we update start time, _cell_changed will be called if column not in [self.COL_TITLE, self.COL_ARTIST]: return - new = self.item(row, column).text() + new_text = self.item(row, column).text() + DEBUG(f"_cell_changed({row=}, {column=}, {new_text=}") - DEBUG(f"_cell_changed({row=}, {column=}, {new=}") - - row_id = self._get_row_id(row) + row_object = self._get_row_object(row) with Session() as session: if row in self._meta_get_notes(): # Save change to database - DEBUG( - f"Notes.update_note: saving new note text '{new=}'", - True - ) - Notes.update_note(session, row_id, row, new) + DEBUG(f"Notes.update_note: saving new note text '{new_text=}'") + row_object.update_note(session, row, new_text) # Set/clear row start time accordingly - try: - start_dt = datetime.strptime(new[-9:], " %H:%M:%S") - start_time = start_dt.time() + start_time = self._get_note_text_time(new_text) + if start_time: self._set_row_start_time(row, start_time) DEBUG( - f"_cell_changed:Note {new} contains valid " + f"_cell_changed:Note {new_text} contains valid " f"time={start_time}" ) - except ValueError: + else: # Reset row start time in case it used to have one self._set_row_start_time(row, None) DEBUG( - f"_ct ell_changed:Note {new} does not contain " + f"_ct ell_changed:Note {new_text} does not contain " "start time" ) else: - track = Tracks.get_by_id(session, row_id) if column == self.COL_ARTIST: - track.update_artist(session, artist=new) + row_object.update_artist(session, artist=new_text) elif column == self.COL_TITLE: - track.update_title(session, title=new) + row_object.update_title(session, title=new_text) else: ERROR("_cell_changed(): unrecognised column") - def _cell_edit_started(self, row, column): - # TODO: V2 check - DEBUG(f"_cell_edit_started({row=}, {column=})") - self.editing_cell = True - self.master_process.disable_play_next_controls() - def _cell_edit_ended(self): - # TODO: V2 check + DEBUG("_cell_edit_ended()") + self.editing_cell = False - # Call repaint to update start times, such as when a note has + # update_display to update start times, such as when a note has # been edited self.update_display() - self.master_process.enable_play_next_controls() + self.parent.enable_play_next_controls() + + def _cell_edit_started(self, row, column): + DEBUG(f"_cell_edit_started({row=}, {column=})") + + self.editing_cell = True + # Disable play controls so that keyboard input doesn't disturb playing + self.parent.disable_play_next_controls() def _delete_rows(self): - # TODO: V2 check - "Delete mutliple rows" + """Delete mutliple rows""" DEBUG("playlist._delete_rows()") @@ -892,9 +799,9 @@ class PlaylistTab(QTableWidget): # delete in reverse row order so row numbers don't # change for row in sorted(rows_to_delete, reverse=True): - id = self._get_row_id(row) + row_object = self._get_row_object(row) if row in notes: - Notes.delete_note(session, id) + row_object.delete_note(session) else: self.remove_track(session, row) self.removeRow(row) @@ -903,7 +810,6 @@ class PlaylistTab(QTableWidget): self.update_display() def _drop_on(self, event): - # TODO: V2 check index = self.indexAt(event.pos()) if not index.isValid(): return self.rowCount() @@ -911,65 +817,41 @@ class PlaylistTab(QTableWidget): return (index.row() + 1 if self._is_below(event.pos(), index) else index.row()) - def _get_row_content(self, row): - """Return content associated with this row""" + @staticmethod + def _get_note_text_time(text): + """Return time specified at the end of text""" - return self.item(row, self.COL_USERDATA).data(CONTENT_OBJECT) - - def _get_row_id(self, row): - # TODO: V2 check - "Return item id as integer from passed row" - - if row is None: - return - if self.item(row, self.COL_INDEX): - try: - return int(self.item(row, self.COL_INDEX).text()) - except TypeError: - ERROR( - f"_get_row_id({row}): error retrieving row id " - f"({self.item(row, self.COL_INDEX).text()})" - ) - else: - ERROR(f"(_get_row_id({row}): no COL_INDEX data in row") - return None - - def _get_row_time(self, row): - # TODO: V2 check try: - if self.item(row, self.COL_START_TIME): - return datetime.strptime(self.item( - row, self.COL_START_TIME).text(), "%H:%M:%S" - ) - else: - return None + return datetime.strptime( + text[-Config.NOTE_TIME_FORMAT:], + Config.NOTE_TIME_FORMAT + ) except ValueError: return None - def _info_row(self, row): - # TODO: V2 check - "Display popup with info re row" + def _get_row_object(self, row): + """Return content associated with this row""" - id = self._get_row_id(row) + return self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + + def _info_row(self, row): + """Display popup with info re row""" + + row_object = self._get_row_object(row) if row in self._meta_get_notes(): - note_text = self.item(row, self.COL_TITLE).text() - txt = f"Note: {note_text}" + txt = row_object.note else: - with Session() as session: - track = Tracks.get_by_id(session, id) - if not track: - txt = f"Track not found (track.id={id})" - else: - txt = ( - f"Title: {track.title}\n" - f"Artist: {track.artist}\n" - f"Track ID: {track.id}\n" - f"Track duration: {helpers.ms_to_mmss(track.duration)}\n" - f"Track fade at: {helpers.ms_to_mmss(track.fade_at)}\n" - f"Track silence at: {helpers.ms_to_mmss(track.silence_at)}" - "\n\n" - f"Path: {track.path}\n" - ) + track = row_object + txt = ( + f"Title: {track.title}\n" + f"Artist: {track.artist}\n" + f"Track ID: {track.id}\n" + f"Track duration: {helpers.ms_to_mmss(track.duration)}\n" + f"Track fade at: {helpers.ms_to_mmss(track.fade_at)}\n" + f"Track silence at: {helpers.ms_to_mmss(track.silence_at)}" + "\n\n" + f"Path: {track.path}\n" + ) info = QMessageBox(self) info.setIcon(QMessageBox.Information) info.setText(txt) @@ -997,14 +879,13 @@ class PlaylistTab(QTableWidget): # Add empty items to unused columns because # colour won't be set for columns without items item = QTableWidgetItem() - self.setItem(row, self.COL_AUTOPLAY, item) + self.setItem(row, self.COL_USERDATA, item) item = QTableWidgetItem() self.setItem(row, self.COL_MSS, item) # Add text of note from title column onwards titleitem = QTableWidgetItem(note.note) self.setItem(row, self.COL_NOTE, titleitem) - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - self.NOTE_COL_SPAN) + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, self.NOTE_COL_SPAN) # Attach note object to row self._set_row_content(row, note) # Mark row as a Note row @@ -1039,9 +920,12 @@ class PlaylistTab(QTableWidget): ) self.insertRow(row) + # Put an item in COL_USERDATA for later + item = QTableWidgetItem() + self.setItem(row, self.COL_USERDATA, item) # Add track details to columns mss_item = QTableWidgetItem(str(track.start_gap)) - if track.start_gap >= 500: + if track.start_gap and track.start_gap >= 500: item.setBackground(QColor(Config.COLOUR_LONG_START)) self.setItem(row, self.COL_MSS, mss_item) @@ -1064,7 +948,7 @@ class PlaylistTab(QTableWidget): start_item = QTableWidgetItem() self.setItem(row, self.COL_START_TIME, start_item) stop_item = QTableWidgetItem() - self.setItem(row, self.COL_STOP_TIME, stop_item) + self.setItem(row, self.COL_END_TIME, stop_item) # Attach track object to row self._set_row_content(row, track) @@ -1072,7 +956,7 @@ class PlaylistTab(QTableWidget): if not os.access(track.path, os.R_OK): self._meta_set_unreadable(row) # Scroll to new row - self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) + self.scrollToItem(title_item, QAbstractItemView.PositionAtCenter) if repaint: self.save_playlist(session) @@ -1081,7 +965,6 @@ class PlaylistTab(QTableWidget): return row def _is_below(self, pos, index): - # TODO: V2 check rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: @@ -1089,14 +972,13 @@ class PlaylistTab(QTableWidget): elif rect.bottom() - pos.y() < margin: return True return ( - rect.contains(pos, True) and not - (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) - and pos.y() >= rect.center().y() # noqa W503 + rect.contains(pos, True) and not + (int(self.model().flags(index)) & Qt.ItemIsDropEnabled) + and pos.y() >= rect.center().y() # noqa W503 ) def _edit_cell(self, mi): - # TODO: V2 check - "Called when table is double-clicked" + """Called when table is double-clicked""" row = mi.row() column = mi.column() @@ -1106,16 +988,16 @@ class PlaylistTab(QTableWidget): self.editItem(item) def _find_next_track_row(self, starting_row=None): - # TODO: V2 check """ - Find next track to play. + Find next track to play. If a starting row is given, start there; + else if there's a track selected, start looking from next track; + otherwise, start from top. Skip rows already played. If not found, return None. If found, return row number. """ - found_next_track = False if starting_row is None: current_row = self._meta_get_current() if current_row is not None: @@ -1123,25 +1005,26 @@ class PlaylistTab(QTableWidget): else: starting_row = 0 notes_rows = self._meta_get_notes() + played_rows = self._meta_get_played() for row in range(starting_row, self.rowCount()): - if row in notes_rows: + if row in notes_rows or row in played_rows: continue - found_next_track = True - break + else: + return row - if found_next_track: - return row - else: - return None + return None - def _meta_clear(self, row): - # TODO: V2 check - "Clear metadata for row" + def _meta_clear_attribute(self, row, attribute): + """Clear given metadata for row""" - self._meta_set(row, None) + if row is None: + raise ValueError(f"_meta_clear_attribute({row=}, {attribute=})") + + new_metadata = self._meta_get(row) ^ attribute + self.item(row, self.COL_USERDATA).setData( + self.ROW_METADATA, new_metadata) def _meta_clear_current(self): - # TODO: V2 check """ Clear current row if there is one. There may not be if we've changed playlists @@ -1149,22 +1032,54 @@ class PlaylistTab(QTableWidget): current_row = self._meta_get_current() if current_row is not None: - self._meta_clear(current_row) + self._meta_clear_attribute(current_row, RowMeta.CURRENT) def _meta_clear_next(self): - # TODO: V2 check - """ - Clear next row if there is one. There may not be if - we've changed playlists """ + Clear next row if there is one. There may not be if + we've changed playlists + """ next_row = self._meta_get_next() if next_row is not None: - self._meta_clear(next_row) + self._meta_clear_attribute(next_row, RowMeta.NEXT) - def _meta_find(self, metadata, one=True): + def _meta_clear_played(self, row): + """Clear played status on row""" - # TODO: V2 check + self._meta_clear_attribute(row, RowMeta.PLAYED) + + def _meta_get(self, row): + """Return row metadata""" + + return self.item(row, self.COL_USERDATA).data(self.ROW_METADATA) + + def _meta_get_current(self): + """Return row marked as current, or None""" + + return self._meta_search(RowMeta.CURRENT) + + def _meta_get_next(self): + """Return row marked as next, or None""" + + return self._meta_search(RowMeta.NEXT) + + def _meta_get_notes(self): + """Return rows marked as notes, or None""" + + return self._meta_search(RowMeta.NOTE, one=False) + + def _meta_get_played(self): + """Return rows marked as played, or None""" + + return self._meta_search(RowMeta.PLAYED, one=False) + + def _meta_get_unreadable(self): + """Return rows marked as unreadable, or None""" + + return self._meta_search(RowMeta.UNREADABLE, one=False) + + def _meta_search(self, metadata, one=True): """ Search rows for metadata. @@ -1176,7 +1091,7 @@ class PlaylistTab(QTableWidget): matches = [] for row in range(self.rowCount()): - if self._meta_get(row) == metadata: + if self._meta_get(row) & metadata: matches.append(row) if not one: @@ -1193,171 +1108,118 @@ class PlaylistTab(QTableWidget): ) raise AttributeError(f"Multiple '{metadata}' metadata {matches}") - def _meta_get(self, row): - # TODO: V2 check - "Return row metadata" + def _meta_set_attribute(self, row, attribute): + """Set row metadata""" - return self.item(row, self.COL_INDEX).data(Qt.UserRole) + if row is None: + raise ValueError(f"_meta_set_attribute({row=}, {attribute=})") - def _meta_get_current(self): - # TODO: V2 check - "Return row marked as current, or None" - - return self._meta_find("current") - - def _meta_get_next(self): - # TODO: V2 check - "Return row marked as next, or None" - - return self._meta_find("next") - - def _meta_get_notes(self): - # TODO: V2 check - "Return rows marked as notes, or None" - - return self._meta_find("note", one=False) - - def _meta_get_unreadable(self): - # TODO: V2 check - "Return rows marked as unreadable, or None" - - return self._meta_find("unreadable", one=False) + new_metadata = self._meta_get(row) | attribute + self.item(row, self.COL_USERDATA).setData( + self.ROW_METADATA, new_metadata) def _meta_set_current(self, row): - # TODO: V2 check - "Mark row as current track" + """Mark this row as current track""" - old_current = self._meta_get_current() - if old_current is not None: - self._meta_clear(old_current) - self._meta_set(row, "current") + self._meta_clear_current() + self._meta_set_attribute(row, RowMeta.CURRENT) def _meta_set_next(self, row): - # TODO: V2 check - "Mark row as next track" + """Mark this row as next track""" - old_next = self._meta_get_next() - if old_next is not None: - self._meta_clear(old_next) - self._meta_set(row, "next") + self._meta_clear_next() + self._meta_set_attribute(row, RowMeta.NEXT) def _meta_set_note(self, row): - # TODO: V2 check - "Mark row as note" + """Mark this row as a note""" - self._meta_set(row, "note") + self._meta_set_attribute(row, RowMeta.NOTE) + + def _meta_set_played(self, row): + """Mark this row as played""" + + self._meta_set_attribute(row, RowMeta.PLAYED) def _meta_set_unreadable(self, row): - # TODO: V2 check - "Mark row as unreadable" + """Mark this row as unreadable""" - self._meta_set(row, "unreadable") - - def _meta_set(self, row, metadata): - # TODO: V2 check - "Set row metadata" - - if self.item(row, self.COL_TITLE): - title = self.item(row, self.COL_TITLE).text() - else: - title = "" - DEBUG( - f"playlist[{TODO: self.id}:{TODO: self.name}]._meta_set(row={row}, " - f"title={title}, metadata={metadata})" - ) - if row is None: - raise ValueError("_meta_set() with row=None") - - self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) + self._meta_set_attribute(row, RowMeta.UNREADABLE) def _set_next(self, row): - # TODO: V2 check """ - If passed row is track row, check track is readable and, if it is, - set that track as the next track to be played and return track_id. - Otherwise return None. + If passed row is track row, check track is readable and, if it is: + - mark that track as the next track to be played + - notify musicmuster + - return track + + Otherwise, return None. """ - DEBUG(f"_set_next({row})") + DEBUG(f"_set_next({row=})") if row in self._meta_get_notes(): return None - track_id = self._get_row_id(row) - if not track_id: + track = self._get_row_object(row) + if not track: return None - if self._track_path_is_readable(track_id): + if self._track_is_readable(track): self._meta_set_next(row) - self.master_process.set_next_track(track_id) + self.parent.set_next_track(track) else: self._meta_set_unreadable(row) - track_id = None + track = None self.update_display() - return track_id + return track def _rescan(self, row): - # TODO: V2 check """ If passed row is track row, rescan it. - Otherwise return None. + Otherwise, return None. """ - DEBUG(f"_rescan({row})") + DEBUG(f"_rescan({row=})") if row in self._meta_get_notes(): return None - track_id = self._get_row_id(row) - if track_id: + track = self._get_row_object(row) + if track: with Session() as session: - track = Tracks.get_by_id(session, track_id) - create_track_from_file(session, track.path) - self._update_row(row, track) + track.rescan(session) + self._update_row(row, track) def _select_event(self): - # TODO: V2 check """ Called when item selection changes. If multiple rows are selected, display sum of durations in status bar. """ rows = set([item.row() for item in self.selectedItems()]) - notes = self._meta_get_notes() - ms = 0 - with Session() as session: - for row in rows: - if row in notes: - continue - ms += Tracks.get_duration(session, self._get_row_id(row)) + note_rows = self._meta_get_notes() + ms = sum([self._get_row_object(row).duration + for row in rows if row not in note_rows]) # Only paint message if there are selected track rows if ms > 0: - self.master_process.lblSumPlaytime.setText( + self.parent.lblSumPlaytime.setText( f"Selected duration: {helpers.ms_to_mmss(ms)}") else: - self.master_process.lblSumPlaytime.setText("") + self.parent.lblSumPlaytime.setText("") def _set_column_widths(self): - # TODO: V2 check # Column widths from settings with Session() as session: for column in range(self.columnCount()): - # Only show column 0 in test mode - # TODO: do we need column zero? Has no width ever. - if (column == 0 and not Config.TESTMODE): - self.setColumnWidth(0, 0) + name = f"playlist_col_{str(column)}_width" + record = Settings.get_int(session, name) + if record and record.f_int is not None: + self.setColumnWidth(column, record.f_int) else: - name = f"playlist_col_{str(column)}_width" - record = Settings.get_int(session, name) - if record and record.f_int is not None: - self.setColumnWidth(column, record.f_int) - else: - self.setColumnWidth(column, - Config.DEFAULT_COLUMN_WIDTH) + self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH) def _set_row_bold(self, row, bold=True): - # TODO: V2 check boldfont = QFont() boldfont.setBold(bold) for j in range(self.columnCount()): @@ -1365,7 +1227,6 @@ class PlaylistTab(QTableWidget): self.item(row, j).setFont(boldfont) def _set_row_colour(self, row, colour): - # TODO: V2 check for j in range(2, self.columnCount()): if self.item(row, j): self.item(row, j).setBackground(colour) @@ -1373,27 +1234,27 @@ class PlaylistTab(QTableWidget): def _set_row_content(self, row, content): """Set content associated with this row""" - self.item(row, self.COL_USERDATA).setData(CONTENT_OBJECT, content) + assert self.item(row, self.COL_USERDATA) - def _set_row_not_bold(self, row): - # TODO: V2 check - self._set_row_bold(row, False) + self.item(row, self.COL_USERDATA).setData(self.CONTENT_OBJECT, content) - def _set_row_end_time(self, row, time): - # TODO: V2 check - "Set passed row end time to passed time" + def _set_row_end_time(self, row, time: Optional[datetime]): + """Set passed row end time to passed time""" try: - time_str = time.strftime("%H:%M:%S") + time_str = time.strftime(Config.NOTE_TIME_FORMAT) except AttributeError: time_str = "" item = QTableWidgetItem(time_str) self.setItem(row, self.COL_END_TIME, item) - def _set_row_start_time(self, row, time): - # TODO: V2 check + def _set_row_not_bold(self, row): + self._set_row_bold(row, False) + + def _set_row_start_time(self, row, time: Optional[datetime]): """Set passed row start time to passed time""" + try: - time_str = time.strftime("%H:%M:%S") + time_str = time.strftime(Config.NOTE_TIME_FORMAT) except AttributeError: time_str = "" item = QTableWidgetItem(time_str) @@ -1416,7 +1277,6 @@ class PlaylistTab(QTableWidget): return False def _update_row(self, row, track): - # TODO: V2 check """ Update the passed row with info from the passed track. """ diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index f41a55f..91d312d 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -175,7 +175,7 @@ border: 1px solid rgb(85, 87, 83); - + 0 @@ -1014,13 +1014,6 @@ border: 1px solid rgb(85, 87, 83); - - - ElideLabel - QLabel -
musicmuster
-
-
diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index cc2d692..0eac72f 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'ui/main_window.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.6 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -99,7 +99,7 @@ class Ui_MainWindow(object): self.hdrCurrentTrack.setWordWrap(True) self.hdrCurrentTrack.setObjectName("hdrCurrentTrack") self.verticalLayout.addWidget(self.hdrCurrentTrack) - self.hdrNextTrack = ElideLabel(self.centralwidget) + self.hdrNextTrack = QtWidgets.QLabel(self.centralwidget) self.hdrNextTrack.setMinimumSize(QtCore.QSize(0, 39)) self.hdrNextTrack.setMaximumSize(QtCore.QSize(16777215, 39)) font = QtGui.QFont() @@ -478,7 +478,7 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) self.tabPlaylist.setCurrentIndex(-1) - self.actionE_xit.triggered.connect(MainWindow.close) + self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -550,5 +550,4 @@ class Ui_MainWindow(object): self.actionSelect_unplayed_tracks.setText(_translate("MainWindow", "Select unplayed tracks")) self.actionAdd_note.setText(_translate("MainWindow", "Add note...")) self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T")) -from musicmuster import ElideLabel import icons_rc diff --git a/app/ui_helpers.py b/app/ui_helpers.py new file mode 100644 index 0000000..caeb12a --- /dev/null +++ b/app/ui_helpers.py @@ -0,0 +1,16 @@ +from PyQt5.QtCore import Qt +from PyQt5.QtGui import QFontMetrics, QPainter + + +class ElideLabel(QLabel): + """ + From https://stackoverflow.com/questions/11446478/ + pyside-pyqt-truncate-text-in-qlabel-based-on-minimumsize + """ + + def paintEvent(self, event): + painter = QPainter(self) + metrics = QFontMetrics(self.font()) + elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) + + painter.drawText(self.rect(), self.alignment(), elided) diff --git a/test_playlists.py b/test_playlists.py index 279f8a4..6a9fceb 100644 --- a/test_playlists.py +++ b/test_playlists.py @@ -1,4 +1,4 @@ -from app.playlists import PlaylistTab +from app.playlists import Notes, PlaylistTab, Tracks from app.models import Playlists @@ -8,3 +8,32 @@ def test_init(qtbot, session): playlist = Playlists(session, "my playlist") playlist_tab = PlaylistTab(None, session, playlist) assert playlist_tab + + +def test_save_and_restore(qtbot, session): + """Playlist with one track, one note, save and restore""" + + # Create playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + + # Insert a note + note_text = "my note" + note_row = 7 + note = Notes(session, playlist.id, note_row, note_text) + playlist_tab._insert_note(session, note) + + # Add a track + track_path = "/a/b/c" + track = Tracks(session, track_path) + playlist_tab._insert_track(session, track) + + # Save playlist + playlist_tab.save_playlist(session) + + # Retrieve playlist + playlists = Playlists.get_open(session) + assert len(playlists) == 1 + retrieved_playlist = playlists[0] + assert track_path in [a.path for a in retrieved_playlist.tracks.values()] + assert note_text in [a.note for a in retrieved_playlist.notes]