From dadd2515872ebe6f0acdb7df613ecf1b2fc87a84 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 4 Apr 2021 12:57:43 +0100 Subject: [PATCH] Refactoring mostly done; manage playlist metadata --- app/config.py | 3 +- app/musicmuster.py | 123 +++++++++++++------------- app/playlists.py | 201 +++++++++++++++++++++++++++++++++--------- app/ui/main_window.ui | 6 +- 4 files changed, 227 insertions(+), 106 deletions(-) diff --git a/app/config.py b/app/config.py index 094cd25..655bdbe 100644 --- a/app/config.py +++ b/app/config.py @@ -10,6 +10,7 @@ class Config(object): COLOUR_EVEN_PLAYLIST = "#d9d9d9" COLOUR_NEXT_HEADER = "#fff3cd" COLOUR_NEXT_PLAYLIST = "#ffc107" + COLOUR_NOTES_PLAYLIST = "#802020" COLOUR_PREVIOUS_HEADER = "#f8d7da" DBFS_FADE = -12 DBFS_SILENCE = -50 @@ -28,7 +29,7 @@ class Config(object): MILLISECOND_SIGFIGS = 0 MYSQL_CONNECT = "mysql+mysqldb://songdb:songdb@localhost/songdb" ROOT = "/home/kae/music" - TIMER_MS = 1000 + TIMER_MS = 500 config = Config diff --git a/app/musicmuster.py b/app/musicmuster.py index 257c64a..f7b4476 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,6 +1,8 @@ #!/usr/bin/python3 import sys +import music + from datetime import datetime, timedelta # from log import DEBUG, ERROR @@ -27,6 +29,9 @@ class Window(QMainWindow, Ui_MainWindow): super().__init__(parent) self.setupUi(self) self.timer = QTimer() + self.even_tick = True + self.music = music.Music() + self.playlist.music = self.music self.connect_signals_slots() self.disable_play_next_controls() @@ -41,9 +46,9 @@ class Window(QMainWindow, Ui_MainWindow): self.setGeometry(x, y, width, height) # Hard code to the only playlist we have for now - self.playlist.load_playlist("Default") - - self.timer.start(Config.TIMER_MS) + if self.playlist.load_playlist("Default"): + self.enable_play_next_controls() + self.timer.start(Config.TIMER_MS) def __del__(self): record = Settings.get_int("mainwindow_height") @@ -83,7 +88,6 @@ class Window(QMainWindow, Ui_MainWindow): self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionFade.triggered.connect(self.fade) self.actionPlay_next.triggered.connect(self.play_next) - self.actionPlay_selected.triggered.connect(self.play_next) self.actionSearch_database.triggered.connect(self.search_database) self.btnPrevious.clicked.connect(self.play_previous) self.btnSearchDatabase.clicked.connect(self.search_database) @@ -94,37 +98,38 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.timeout.connect(self.tick) def disable_play_next_controls(self): - self.actionPlay_selected.setEnabled(False) self.actionPlay_next.setEnabled(False) def enable_play_next_controls(self): - self.actionPlay_selected.setEnabled(True) self.actionPlay_next.setEnabled(True) def fade(self): self.playlist.fade() def play_next(self): - self.music.play_next() - self.current_track.setText( - f"{self.music.get_current_title()} - " - f"{self.music.get_current_artist()}" - ) + self.playlist.play_next() + self.disable_play_next_controls() self.previous_track.setText( - f"{self.music.get_previous_title()} - " - f"{self.music.get_previous_artist()}" + f"{self.playlist.get_previous_title()} - " + f"{self.playlist.get_previous_artist()}" + ) + self.current_track.setText( + f"{self.playlist.get_current_title()} - " + f"{self.playlist.get_current_artist()}" + ) + self.next_track.setText( + f"{self.playlist.get_next_title()} - " + f"{self.playlist.get_next_artist()}" ) - self.set_next_track() # Set time clocks now = datetime.now() self.label_start_tod.setText(now.strftime("%H:%M:%S")) - silence_at = self.music.get_current_silence_at() + silence_at = self.playlist.get_current_silence_at() silence_time = now + timedelta(milliseconds=silence_at) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_fade_length.setText(helpers.ms_to_mmss( - silence_at - self.music.get_current_fade_at())) - self.set_playlist_colours() + silence_at - self.playlist.get_current_fade_at())) def play_previous(self): "Resume playing last track" @@ -136,58 +141,54 @@ class Window(QMainWindow, Ui_MainWindow): dlg = DbDialog(self) dlg.exec() + def set_next_track(self): + # TODO + pass + def tick(self): - pass - # self.current_time.setText(now.strftime("%H:%M:%S")) - # if self.music.playing(): - # playtime = self.music.get_current_playtime() - # self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) - # self.label_fade_timer.setText( - # helpers.ms_to_mmss(self.music.get_current_fade_at() - playtime) - # ) - # self.label_silent_timer.setText( - # helpers.ms_to_mmss( - # self.music.get_current_silence_at() - playtime)) - # self.label_end_timer.setText( - # helpers.ms_to_mmss( - # self.music.get_current_duration() - playtime)) - # else: - # # When music ends, ensure next track is selected - # if self.playlist.selectionModel().hasSelection(): - # row = self.playlist.currentRow() - # track_id = int(self.playlist.item(row, 0).text()) - # if track_id == self.music.get_current_track_id(): - # # Current track highlighted: select next - # try: - # self.playlist.selectRow(row + 1) - # except AttributeError: - # # TODO - # pass + """ + Update screen + The Time of Day clock is updated every tick (500ms). - def update_tod_clock(self): - "Update time of day clock" + All other timers are updated every second. As the timers have a + one-second resolution, updating every 500ms can result in some + timers updating and then, 500ms later, other timers updating. That + looks odd. + """ - # TODO - pass + now = datetime.now() - def update_track_headers(self): - "Update last/current/next track header" + self.lblTOD.setText(now.strftime("%H:%M:%S")) - # TODO - pass + self.even_tick = not self.even_tick + if not self.even_tick: + return - def update_track_clocks(self): - "Update started at, silent at, clock times" + if self.music.playing(): + playtime = self.music.get_playtime() + self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) + self.label_fade_timer.setText( + helpers.ms_to_mmss( + self.playlist.get_current_fade_at() - playtime) + ) + time_to_silence = ( + self.playlist.get_current_silence_at() - playtime + ) + if time_to_silence < 500: + self.label_silent_timer.setText("00:00") + self.enable_play_next_controls() + else: + self.label_silent_timer.setText( + helpers.ms_to_mmss(time_to_silence) + ) + self.label_end_timer.setText( + helpers.ms_to_mmss( + self.playlist.get_current_duration() - playtime)) + else: + # When music ends, update playlist display + self.playlist.update_playlist_colours() - # TODO - pass - - def update_track_timers(self): - "Update elapsed time, etc, timers" - - # TODO - pass class DbDialog(QDialog): def __init__(self, parent=None): diff --git a/app/playlists.py b/app/playlists.py index 1920bfe..314ff65 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -10,11 +10,10 @@ from PyQt5.QtWidgets import ( ) import helpers -import music from config import Config -from log import DEBUG -from model import Playlists, Settings, Tracks +from log import DEBUG, ERROR +from model import Playdates, Playlists, Settings, Tracks class Playlist(QTableWidget): @@ -27,11 +26,6 @@ class Playlist(QTableWidget): COL_ENDTIME = 5 COL_PATH = 6 - # UserRoles - NEXT_TRACK = 1 - CURRENT_TRACK = 2 - COMMENT = 3 - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -45,8 +39,6 @@ class Playlist(QTableWidget): self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) - self.music = music.Music() - self.current_track = None self.next_track = None self.previous_track_path = None @@ -136,9 +128,13 @@ class Playlist(QTableWidget): self.item(drop_row + row_index, column_index).setSelected(True) super().dropEvent(event) + DEBUG(f"Moved row(s) {rows} to become row {drop_row}") - def face(self): + self.clearSelection() + self.update_playlist_colours() + + def fade(self): self.music.fade() def get_current_artist(self): @@ -220,14 +216,100 @@ class Playlist(QTableWidget): ) def load_playlist(self, name): - "Load tracks from named playlist" + """ + Load tracks from named playlist. + + Set first track as next track to play. + + Return True if successful else False. + """ for track in Playlists.get_playlist_by_name(name).get_tracks(): self.add_to_playlist(track) + # Set the first playable track as next to play for row in range(self.rowCount()): if self.set_row_as_next_track(row): - break + self.meta_set_next(row) + return True + + self.update_playlist_colours() + + return False + + def meta_clear(self, row): + "Clear metad ata for row" + + self.meta_set(row, None) + + def meta_find(self, metadata, one=True): + """ + Search rows for metadata. + + If one is True, check that only one row matches and return + the row number. + + If one is False, return a list of matching row numbers. + """ + + matches = [] + for row in range(self.rowCount()): + if self.meta_get(row) == metadata: + matches.append(row) + + if not one: + return matches + + if len(matches) == 0: + return None + elif len(matches) == 1: + return matches[0] + else: + ERROR( + f"Multiple matches for metadata '{metadata}' found " + f"in rows: {', '.join([str(x) for x in matches])}" + ) + raise AttributeError(f"Multiple '{metadata}' metadata {matches}") + + def meta_get(self, row): + "Return row metadata" + + return self.item(row, self.COL_INDEX).data(Qt.UserRole) + + def meta_get_current(self): + "Return row marked as current, or None" + + return self.meta_find("current") + + def meta_get_next(self): + "Return row marked as next, or None" + + return self.meta_find("next") + + def meta_get_notes(self): + "Return rows marked as notes, or None" + + return self.meta_find("note", one=False) + + def meta_set_current(self, row): + "Mark row as current track" + + self.meta_set(row, "current") + + def meta_set_next(self, row): + "Mark row as next track" + + self.meta_set(row, "next") + + def meta_set_note(self, row): + "Mark row as note" + + self.meta_set(row, "note") + + def meta_set(self, row, metadata): + "Set row metadata" + + self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) def play_next(self): """ @@ -236,34 +318,68 @@ class Playlist(QTableWidget): If there is no next track set, return. If there's currently a track playing, fade it. Move next track to current track. - Cue up next track in playlist if there is one. Play (new) current. + Update playlist "current track" metadata + Cue up next track in playlist if there is one. + Update playlist "next track" metadata Tell database to record it as played - Remember it was played this session + Remember it was played for this session Update playlist appearance. """ + # If there is no next track set, return. if not self.next_track: return + # If there's currently a track playing, fade it. if self.music.playing(): path, position = self.music.fade() self.previous_track_path = path self.previous_track_position = position + # Move next track to current track. self.current_track = self.next_track + + # Play (new) current. self.music.play(self.current_track.path) - # Find the playlist row for current track - current_track_id = self.current_track.track_id - self.current_track_row = None - for row in range(self.rowCount): - if self.item(row, 0): - if current_track_id == int(self.item(row, 0).text()): - self.current_track_row = row - break + # Update playlist "current track" metadata + old_current = self.meta_get_current() + if old_current is not None: + self.meta_clear(old_current) + current = self.meta_get_next() + if current is not None: + self.meta_set_current(current) - self.next_track = None + # Cue up next track in playlist if there is one. + track_id = 0 + next_row = 0 + if current is not None: + start = current + 1 + else: + start = 0 + for row in range(start, self.rowCount()): + if self.item(row, self.COL_INDEX): + if int(self.item(row, self.COL_INDEX).text()) > 0: + track_id = int(self.item(row, self.COL_INDEX).text()) + next_row = row + break + if track_id: + self.next_track = Tracks.get_track(track_id) + + # Update playlist "next track" metadata + if next_row: + self.meta_set_next(next_row) + + # 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.played_tracks.append(self.current_track.id) + + # Update playlist appearance. + self.update_playlist_colours() def set_next_track(self): """ @@ -302,30 +418,33 @@ class Playlist(QTableWidget): DEBUG(f"set_row_as_next_track: track_id={track_id}") self.next_track = Tracks.get_track(track_id) # Mark row as next track - self.item(row, self.COL_INDEX).setData( - Qt.UserRole, self.NEXT_TRACK) - self.set_playlist_colours() + self.meta_set_next(row) return True return False - def set_playlist_colours(self): + def update_playlist_colours(self): self.clearSelection() + current = self.meta_get_current() + next = self.meta_get_next() + notes = self.meta_get_notes() + for row in range(self.rowCount()): - if self.item(row, self.COL_INDEX): - data = self.item(row, self.COL_INDEX).data(Qt.UserRole) - if data == self.NEXT_TRACK: - self.set_row_colour( - row, QColor(Config.COLOUR_NEXT_PLAYLIST)) - elif data == self.CURRENT_TRACK: - self.set_row_colour( - row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) + if row == current: + self.set_row_colour( + row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) + elif row == next: + self.set_row_colour( + row, QColor(Config.COLOUR_NEXT_PLAYLIST)) + elif row in notes: + self.set_row_colour( + row, QColor(Config.COLOUR_NOTES_PLAYLIST)) + else: + if row % 2: + colour = QColor(Config.COLOUR_ODD_PLAYLIST) else: - if row % 2: - colour = QColor(Config.COLOUR_ODD_PLAYLIST) - else: - colour = QColor(Config.COLOUR_EVEN_PLAYLIST) - self.set_row_colour(row, colour) + colour = QColor(Config.COLOUR_EVEN_PLAYLIST) + self.set_row_colour(row, colour) def set_row_colour(self, row, colour): for j in range(self.columnCount()): diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 3c5f4b9..646aeff 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -723,8 +723,8 @@ border: 1px solid rgb(85, 87, 83); Pla&ylist - + @@ -745,7 +745,7 @@ border: 1px solid rgb(85, 87, 83); background-color: rgb(211, 215, 207); - + icon-play.pngicon-play.png @@ -757,7 +757,7 @@ border: 1px solid rgb(85, 87, 83); Return - + icon-play-next.pngicon-play-next.png