diff --git a/app/model.py b/app/model.py index f67eab1..9906547 100644 --- a/app/model.py +++ b/app/model.py @@ -169,11 +169,11 @@ class Playlists(Base): @staticmethod def new(name): DEBUG(f"Playlists.new(name={name})") - pl = Playlists() - pl.name = name - session.add(pl) + playlist = Playlists() + playlist.name = name + session.add(playlist) session.commit() - return pl.id + return playlist @staticmethod def open(plid): @@ -184,11 +184,10 @@ class Playlists(Base): p.last_used = datetime.now() session.commit() - def close(plid): + def close(self): "Record playlist as no longer loaded" - p = session.query(Playlists).filter(Playlists.id == plid).one() - p.loaded = False + self.loaded = False session.commit() @staticmethod diff --git a/app/music.py b/app/music.py index c21a9e7..36cff7a 100644 --- a/app/music.py +++ b/app/music.py @@ -26,8 +26,6 @@ class Music: """ Fade the currently playing track. - Return the current track position. - The actual management of fading runs in its own thread so as not to hold up the UI during the fade. """ @@ -39,13 +37,9 @@ class Music: self.fading = True - position = self.player.get_position() - thread = threading.Thread(target=self._fade) thread.start() - return position - def _fade(self): """ Implementation of fading the current track in a separate thread. @@ -74,7 +68,7 @@ class Music: p.audio_set_volume(int(self.max_volume * volume_factor)) sleep(sleep_time) - p.pause() + p.stop() p.release() self.fading = False @@ -83,6 +77,11 @@ class Music: return self.player.get_time() + def get_position(self): + "Return current position" + + return self.player.get_position() + def play(self, path): """ Start playing the track at path. diff --git a/app/musicmuster.py b/app/musicmuster.py index d796c8b..faa89e9 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -46,7 +46,9 @@ class Window(QMainWindow, Ui_MainWindow): self.music = music.Music() self.current_track = None + self.current_track_playlist = None self.next_track = None + self.next_track_playlist = None self.previous_track = None self.previous_track_position = None self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT) @@ -54,7 +56,7 @@ class Window(QMainWindow, Ui_MainWindow): self.menuTest.menuAction().setVisible(Config.TESTMODE) self.set_main_window_size() - self.current_playlist = self.tabPlaylist.currentWidget + self.visible_playlist = self.tabPlaylist.currentWidget self.load_last_playlists() self.enable_play_next_controls() @@ -70,7 +72,7 @@ class Window(QMainWindow, Ui_MainWindow): if dlg.exec_(): for fname in dlg.selectedFiles(): track = add_path_to_db(fname) - self.current_playlist().add_to_playlist(track) + self.visible_playlist()._add_to_playlist(track) def set_main_window_size(self): @@ -85,8 +87,8 @@ class Window(QMainWindow, Ui_MainWindow): self.setGeometry(x, y, width, height) def clear_selection(self): - if self.current_playlist(): - self.current_playlist().clearSelection() + if self.visible_playlist(): + self.visible_playlist().clearSelection() def closeEvent(self, event): "Don't allow window to close when a track is playing" @@ -152,7 +154,8 @@ class Window(QMainWindow, Ui_MainWindow): dlg.resize(500, 100) ok = dlg.exec() if ok: - self.current_playlist().create_playlist(dlg.textValue()) + playlist = Playlists.new(dlg.textValue()) + self.load_playlist(playlist) def change_volume(self, volume): "Change player maximum volume" @@ -162,7 +165,7 @@ class Window(QMainWindow, Ui_MainWindow): self.music.set_volume(volume) def close_playlist(self): - Playlists.close(self.current_playlist().id) + self.visible_playlist().db.close() index = self.tabPlaylist.currentIndex() self.tabPlaylist.removeTab(index) @@ -192,22 +195,25 @@ class Window(QMainWindow, Ui_MainWindow): dlg.resize(500, 100) ok = dlg.exec() if ok: - self.current_playlist().add_note(dlg.textValue()) + self.visible_playlist().add_note(dlg.textValue()) def load_last_playlists(self): "Load the playlists that we loaded at end of last session" - for p in Playlists.get_last_used(): - playlist = Playlist() - playlist.load_playlist(p.id) - last_tab = self.tabPlaylist.addTab(playlist, p.name) + for playlist in Playlists.get_last_used(): + DEBUG(f"load_last_playlists(), {playlist.name=}, {playlist.id=}") + self.load_playlist(playlist) - # Set last tab as active - self.tabPlaylist.setCurrentIndex(last_tab) - # Get next track - self.next_track = Tracks.get_track( - self.current_playlist().get_next_track_id()) - self.update_headers() + def load_playlist(self, playlist_db): + """ + Take the passed database object, create a playlist display, attach + the database object, get it populated and then add tab. + """ + + playlist_table = Playlist() + playlist_table.db = playlist_db + playlist_table.populate() + self.tabPlaylist.addTab(playlist_table, playlist_db.name) def play_next(self): """ @@ -235,38 +241,34 @@ class Window(QMainWindow, Ui_MainWindow): f"{self.current_track.title if self.current_track else None}" ) - # If there's currently a track playing, fade it. - if self.music.playing(): - self.previous_track_position = self.music.fade() - self.previous_track = self.current_track + # Stop current track, if any + self.stop_playing() - # Shuffle tracks along + # Play next track self.current_track = self.next_track + self.current_track_playlist = self.next_track_playlist self.next_track = None - - # Play (new) current. + self.next_track_playlist = None self.music.play(self.current_track.path) # Update metadata - next_track_id = self.current_playlist().started_playing_next() + next_track_id = self.current_track_playlist.play_started() if next_track_id is not None: self.next_track = Tracks.get_track(next_track_id) + self.next_track_playlist = self.current_track_playlist # Check we can read it if not os.access(self.next_track.path, os.R_OK): self.show_warning( "Can't read next track", self.next_track.path) else: - self.next_track = None + self.next_track = self.next_track_playlist = None # Tell database to record it as played self.current_track.update_lastplayed() Playdates.add_playdate(self.current_track) - # Remember it was played for this session - self.current_playlist().mark_track_played(self.current_track.id) - self.disable_play_next_controls() self.update_headers() @@ -286,32 +288,25 @@ class Window(QMainWindow, Ui_MainWindow): # TODO pass - def playlist_changed(self): - "The playlist has changed (probably because the user changed tabs)" - - self.next_track = Tracks.get_track( - self.current_playlist().get_next_track_id()) - self.update_headers() - def search_database(self): dlg = DbDialog(self) dlg.exec() def select_playlist(self): + # TODO don't show those that are currently open dlg = SelectPlaylistDialog(self) dlg.exec() - playlist = Playlist() - plid = dlg.plid - dlg.close() - playlist.load_playlist(plid) - new_tab = self.tabPlaylist.addTab(playlist, Playlists.get_name(plid)) - self.tabPlaylist.setCurrentIndex(new_tab) + playlist = Playlists.get_playlist_by_id(dlg.plid) + self.load_playlist(playlist) def set_next_track(self): "Set selected track as next" - next_track_id = self.current_playlist().set_selected_as_next() + next_track_id = self.visible_playlist().set_selected_as_next() if next_track_id: + if self.next_track_playlist: + self.next_track_playlist.clear_next() + self.next_track_playlist = self.visible_playlist() self.next_track = Tracks.get_track(next_track_id) self.update_headers() @@ -323,7 +318,7 @@ class Window(QMainWindow, Ui_MainWindow): def songfacts_search(self): "Open a browser window in Songfacts searching for selected title" - title = self.current_playlist().get_selected_title() + title = self.visible_playlist().get_selected_title() if title: slug = slugify(title, replacements=([["'", ""]])) url = f"https://www.songfacts.com/search/songs/{slug}" @@ -332,15 +327,34 @@ class Window(QMainWindow, Ui_MainWindow): def stop(self): "Stop playing immediately" + DEBUG("musicmuster.stop()") + + self.stop_playing(fade=False) + + def stop_playing(self, fade=True): + "Stop playing current track" + + DEBUG("musicmuster.stop_playing()") + + if not self.music.playing(): + return + + self.previous_track_position = self.music.get_position() + if fade: + self.music.fade() + else: + self.music.stop() + self.current_track_playlist.clear_current() + + # Shuffle tracks along self.previous_track = self.current_track - self.previous_track_position = self.music.stop() + + self.update_headers() def tab_change(self): "User has changed tabs, so refresh next track" - self.next_track = Tracks.get_track( - self.current_playlist().get_next_track_id()) - self.update_headers() + pass def test_function(self): "Placeholder for test function" @@ -427,11 +441,12 @@ class Window(QMainWindow, Ui_MainWindow): if self.playing: self.label_end_timer.setText("00:00") self.frame_silent.setStyleSheet("") + self.current_track_playlist.play_stopped() self.playing = False self.previous_track = self.current_track self.current_track = None + self.current_track_playlist = None self.previous_track_position = 0 - self.current_playlist().stopped_playing() self.update_headers() def update_headers(self): @@ -464,7 +479,7 @@ class Window(QMainWindow, Ui_MainWindow): def wikipedia_search(self): "Open a browser window in Wikipedia searching for selected title" - title = self.current_playlist().get_selected_title() + title = self.visible_playlist().get_selected_title() if title: str = urllib.parse.quote_plus(title) url = f"https://www.wikipedia.org/w/index.php?search={str}" @@ -531,7 +546,7 @@ class DbDialog(QDialog): def add_track(self, track_id): track = Tracks.track_from_id(track_id) - self.parent().current_playlist().add_to_playlist(track) + self.parent().visible_playlist()._add_to_playlist(track) # Select search text to make it easier for next search self.select_searchtext() diff --git a/app/playlists.py b/app/playlists.py index 762e4a6..dcda704 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -17,7 +17,7 @@ import os from config import Config from datetime import datetime, timedelta from log import DEBUG, ERROR -from model import Notes, Playlists, PlaylistTracks, Settings, Tracks +from model import Notes, PlaylistTracks, Settings, Tracks class Playlist(QTableWidget): @@ -80,7 +80,6 @@ class Playlist(QTableWidget): self.customContextMenuRequested.connect(self._context_menu) self.viewport().installEventFilter(self) - self.id = None self.current_track_start_time = None self.played_tracks = [] @@ -138,7 +137,6 @@ class Playlist(QTableWidget): f"Moved row(s) {rows} to become row {drop_row}" ) - self._save_playlist() self._repaint() def eventFilter(self, source, event): @@ -163,7 +161,7 @@ class Playlist(QTableWidget): # ########## Externally called functions ########## - def add_note(self, text): + def add_note(self, note, repaint=True): """ Add note to playlist @@ -177,89 +175,93 @@ class Playlist(QTableWidget): row = self.rowCount() DEBUG(f"playlist.add_note(): row={row}") - note = Notes.add_note(self.id, row, text) - self.add_to_playlist(note, row=row) - - def add_to_playlist(self, data, repaint=True, row=None): - """ - Add data to playlist. Data may be either a Tracks object or a - Notes object. - """ - - DEBUG(f"add_to_playlist(data={data}, repaint={repaint}, row={row}") - if not row: - if self.selectionModel().hasSelection(): - row = self.currentRow() - else: - row = self.rowCount() - DEBUG(f"add_to_playlist: row set to {row}") + # Does note end with a time? + start_time = None + try: + start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time() + DEBUG(f"Note contains valid time={start_time}") + except ValueError: + DEBUG( + f"Note on row {row} ('{note.note}') " + "does not contain valid time" + ) self.insertRow(row) - if isinstance(data, Tracks): - DEBUG(f"add_to_playlist: track.id={data.id}") - track = data - item = QTableWidgetItem(str(track.id)) - self.setItem(row, self.COL_INDEX, item) - item = QTableWidgetItem(str(track.start_gap)) - self.setItem(row, self.COL_MSS, item) - titleitem = QTableWidgetItem(track.title) - self.setItem(row, self.COL_TITLE, titleitem) - item = QTableWidgetItem(track.artist) - self.setItem(row, self.COL_ARTIST, item) - item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) - self.setItem(row, self.COL_DURATION, item) - item = QTableWidgetItem(track.path) - self.setItem(row, self.COL_PATH, item) - elif isinstance(data, Notes): - DEBUG(f"add_to_playlist: note.id={data.id}") - note = data + item = QTableWidgetItem(str(note.id)) + 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) - # Does note end with a time? - start_time = None - try: - start_time = datetime.strptime( - note.note[-9:], " %H:%M:%S").time() - DEBUG(f"Note contains valid time={start_time}") - except ValueError: - DEBUG( - f"Note on row {row} ('{note.note}') " - "does not contain valid time" - ) + # Add start times or empty items as background + # colour won't be set for columns without items + self._set_row_time(row, start_time) + item = QTableWidgetItem() + self.setItem(row, self.COL_PATH, item) - item = QTableWidgetItem(str(note.id)) - self.setItem(row, self.COL_INDEX, item) - titleitem = QTableWidgetItem(data.note) - self.setItem(row, self.COL_NOTE, titleitem) - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - self.NOTE_COL_SPAN) - - # Add start times or empty items as background - # colour won't be set for columns without items - self._set_row_time(row, start_time) - item = QTableWidgetItem() - self.setItem(row, self.COL_PATH, item) - - self._meta_set_note(row) - - else: - ERROR(f"Unknown data passed to add_to_playlist({data})") - return + self._meta_set_note(row) # Scroll to new row self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) if repaint: - self._save_playlist() self._repaint(clear_selection=False) - def create_playlist(self, name): - "Create new playlist" + def add_track(self, track, repaint=True): + """ + Add track to playlist - new_id = Playlists.new(name) - self.load_playlist(new_id) + If a row is selected, add track above. Otherwise, add to end of + playlist. + """ + + if self.selectionModel().hasSelection(): + row = self.currentRow() + else: + row = self.rowCount() + DEBUG(f"add_track({track=}), {row=}") + + self.insertRow(row) + + item = QTableWidgetItem(str(track.id)) + self.setItem(row, self.COL_INDEX, item) + item = QTableWidgetItem(str(track.start_gap)) + self.setItem(row, self.COL_MSS, item) + titleitem = QTableWidgetItem(track.title) + self.setItem(row, self.COL_TITLE, titleitem) + item = QTableWidgetItem(track.artist) + self.setItem(row, self.COL_ARTIST, item) + item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) + self.setItem(row, self.COL_DURATION, item) + item = QTableWidgetItem(track.path) + self.setItem(row, self.COL_PATH, item) + # Add empty start time for now as background + # colour won't be set for columns without items + item = QTableWidgetItem() + self.setItem(row, self.COL_PATH, item) + + # Scroll to new row + self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) + + if repaint: + self._repaint(clear_selection=False) + + def clear_current(self): + "Clear current track" + + self._meta_clear_current() + self._repaint(save_playlist=False) + + def clear_next(self): + "Clear next track" + + self._meta_clear_next() + self._repaint(save_playlist=False) def get_next_track_id(self): + "Return next track id" next_row = self._meta_get_next() return self._get_row_id(next_row) @@ -273,24 +275,30 @@ class Playlist(QTableWidget): else: return None - def load_playlist(self, plid): + def play_started(self): """ - Load tracks and notes from playlist id. - - If this is not the first playlist loaded, we may already have - a next track set in which case don't change it. Otherwise, set - the first non-notes row as next track to play. + Update current track to be what was next, and determine next track. + Return next track_id. """ - DEBUG(f"load_playlist(plid={plid})") + self.current_track_start_time = datetime.now() - self.id = plid - Playlists.open(plid) - p = Playlists.get_playlist_by_id(plid) - self.db = p - self.populate_playlist() + current_row = self._meta_get_next() + self._meta_set_current(current_row) + self.played_tracks.append(current_row) - def populate_playlist(self): + # Scroll to put current track in centre + scroll_to = self.item(current_row, self.COL_INDEX) + self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter) + next_track_id = self._mark_next_track() + self._repaint(save_playlist=False) + return next_track_id + + def play_stopped(self): + self._meta_clear_current() + self.current_track_start_time = None + + def populate(self): # add them in row order. 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 lower-numbered ones. @@ -306,15 +314,7 @@ class Playlist(QTableWidget): # Now add data in row order for item in sorted(data, key=lambda x: x[0]): - self.add_to_playlist(item[1], repaint=False) - - # Set next track for this playlist - notes_rows = self._meta_get_notes() - for row in range(self.rowCount()): - if row in notes_rows: - continue - self._meta_set_next(row) - break + self._add_to_playlist(item[1], repaint=False) # Scroll to top scroll_to = self.item(0, self.COL_INDEX) @@ -322,9 +322,6 @@ class Playlist(QTableWidget): self._repaint() - def mark_track_played(self, track_id): - self.played_tracks.append(track_id) - def set_selected_as_next(self): """ Sets the selected track as the next track. @@ -335,26 +332,21 @@ class Playlist(QTableWidget): return self._set_next(self.currentRow()) - def started_playing_next(self): - """ - Update current track to be what was next, and determine next track. - Return next track_id. - """ - - self.current_track_start_time = datetime.now() - current_row = self._meta_get_next() - self._meta_set_current(current_row) - # Scroll to put current track in centre - scroll_to = self.item(current_row, self.COL_INDEX) - self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter) - return self._mark_next_track() - - def stopped_playing(self): - self._meta_clear_current() - self.current_track_start_time = None - # ########## Internally called functions ########## + def _add_to_playlist(self, data, repaint=True): + """ + Add data to playlist. Data may be either a Tracks object or a + Notes object. + """ + + DEBUG(f"_add_to_playlist({data=})") + + if isinstance(data, Tracks): + self.add_track(data, repaint=repaint) + elif isinstance(data, Notes): + self.add_note(data, repaint=repaint) + def _calculate_next_start_time(self, row, start): "Return this row's end time given its start time" @@ -404,7 +396,7 @@ class Playlist(QTableWidget): if row in self._meta_get_notes(): Notes.delete_note(id) else: - PlaylistTracks.remove_track(self.id, row) + PlaylistTracks.remove_track(self.db.id, row) self.removeRow(row) self._repaint() @@ -484,8 +476,6 @@ class Playlist(QTableWidget): if not found_next_track: return None - self._repaint() - track_id = self._get_row_id(row) return track_id @@ -501,9 +491,19 @@ class Playlist(QTableWidget): """ current_row = self._meta_get_current() - if current_row: + if current_row is not None: self._meta_clear(current_row) + def _meta_clear_next(self): + """ + 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) + def _meta_find(self, metadata, one=True): """ Search rows for metadata. @@ -581,7 +581,10 @@ class Playlist(QTableWidget): title = self.item(row, self.COL_TITLE).text() else: title = "" - DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})") + DEBUG( + f"playlist[{self.db.id}:{self.db.name}]._meta_set(row={row}, " + f"title={title}, metadata={metadata})" + ) if row is None: raise ValueError("_meta_set() with row=None") @@ -600,20 +603,26 @@ class Playlist(QTableWidget): if self.item(row, self.COL_INDEX): self._meta_set_next(row) - self._repaint() + self._repaint(save_playlist=False) return self._get_row_id(row) else: return None - def _repaint(self, clear_selection=True): + def _repaint(self, clear_selection=True, save_playlist=True): "Set row colours, fonts, etc, and save playlist" - DEBUG(f"_repaint(clear_selection={clear_selection})") + DEBUG( + f"playlist[{self.db.id}:{self.db.name}]." + f"_repaint({clear_selection=}, {save_playlist=})" + ) if clear_selection: self.clearSelection() + if save_playlist: + self._save_playlist() + current = self._meta_get_current() - next = self._meta_get_next() or 0 + next = self._meta_get_next() notes = self._meta_get_notes() # Set colours and start times @@ -697,8 +706,6 @@ class Playlist(QTableWidget): times in one playlist and in multiple playlists. """ - playlist = Playlists.get_playlist_by_id(self.id) - # Notes first # Create dictionaries indexed by note_id playlist_notes = {} @@ -714,7 +721,7 @@ class Playlist(QTableWidget): playlist_notes[note_id] = row # Database - for note in playlist.notes: + for note in self.db.notes: database_notes[note.id] = note.row # Notes to add to database @@ -723,14 +730,14 @@ class Playlist(QTableWidget): for note_id in set(playlist_notes.keys()) - set(database_notes.keys()): ERROR( f"_save_playlist(): Note.id={note_id} " - f"missing from playlist {playlist} in database" + f"missing from playlist {self} in database" ) # Notes to remove from database for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): DEBUG( f"_save_playlist(): Delete note note_id={note_id} " - f"from playlist {playlist} in database" + f"from playlist {self} in database" ) Notes.delete_note(note_id) @@ -756,7 +763,7 @@ class Playlist(QTableWidget): playlist_tracks[row] = self._get_row_id(row) # Database - for track in playlist.tracks: + for track in self.db.tracks: database_tracks[track.row] = track.track_id # Tracks rows to add to database @@ -764,7 +771,7 @@ class Playlist(QTableWidget): set(set(playlist_tracks.keys()) - set(database_tracks.keys())) ): DEBUG(f"_save_playlist(): row {row} missing from database") - PlaylistTracks.add_track(self.id, playlist_tracks[row], row) + PlaylistTracks.add_track(self.db.id, playlist_tracks[row], row) # Track rows to remove from database for row in ( @@ -775,7 +782,7 @@ class Playlist(QTableWidget): f"_save_playlist(): row {row} in database not playlist " f"(track={track})" ) - PlaylistTracks.remove_track(playlist.id, row) + PlaylistTracks.remove_track(self.db.id, row) # Track rows to update in database for row in ( @@ -784,11 +791,11 @@ class Playlist(QTableWidget): if playlist_tracks[row] != database_tracks[row]: DEBUG( "_save_playlist(): Update row={row} in database for " - f"playlist {playlist} from track={database_tracks[row]} " + f"playlist {self} from track={database_tracks[row]} " f"to track={playlist_tracks[row]}" ) PlaylistTracks.update_row_track( - self.id, row, playlist_tracks[row]) + self.db.id, row, playlist_tracks[row]) def _set_column_widths(self):