diff --git a/app/model.py b/app/model.py index 9bfca44..bdd60b9 100644 --- a/app/model.py +++ b/app/model.py @@ -147,13 +147,26 @@ class Playlists(Base): def __repr__(self): return (f"") - # Currently we only support one playlist, so make that obvious from - # function name - @classmethod - def get_playlist_by_name(cls, name): - "Returns a playlist object for named playlist" + @staticmethod + def new(name): + DEBUG(f"Playlists.new({name})") + pl = Playlists() + pl.name = name + session.add(pl) + session.commit() + return pl.id - return session.query(Playlists).filter(Playlists.name == name).one() + @staticmethod + def get_all_playlists(): + "Returns a list of (id, name) of all playlists" + + return session.query(Playlists).all() + + @classmethod + def get_playlist_by_id(cls, plid): + "Returns a playlist object for playlist id" + + return session.query(Playlists).filter(Playlists.id == plid).one() def add_track(self, track, row): glue = PlaylistTracks(row=row) @@ -178,14 +191,18 @@ class PlaylistTracks(Base): playlists = relationship("Playlists", back_populates="tracks") @staticmethod - def update_track_row(playlist_id, track_id, row): - DEBUG(f"update_track({id}, {row})") + def update_track_row(playlist_id, track_id, old_row, new_row): + DEBUG( + f"update_track_row({playlist_id}, " + f"{track_id}, {old_row}, {new_row})" + ) plt = session.query(PlaylistTracks).filter( PlaylistTracks.playlist_id == playlist_id, - PlaylistTracks.track_id == track_id + PlaylistTracks.track_id == track_id, + PlaylistTracks.row == old_row ).one() - plt.row = row + plt.row = new_row session.commit() @staticmethod diff --git a/app/musicmuster.py b/app/musicmuster.py index 1553c26..9a686ad 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -7,21 +7,19 @@ import music from datetime import datetime, timedelta # from log import DEBUG -from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtCore import QTimer from PyQt5.QtWidgets import ( QApplication, - QDialog, QFileDialog, QInputDialog, - QListWidgetItem, + QLabel, QMainWindow, ) import helpers from config import Config -from model import Playlists, Settings, Tracks -from ui.dlg_search_database_ui import Ui_Dialog +from model import Settings from ui.main_window_ui import Ui_MainWindow @@ -47,11 +45,16 @@ class Window(QMainWindow, Ui_MainWindow): height = record.f_int or 981 self.setGeometry(x, y, width, height) - # Hard code to the only playlist we have for now - if self.playlist.load_playlist("Default"): + # Hard code to the first playlist for now + # TODO + if self.playlist.load_playlist(1): self.update_headers() self.enable_play_next_controls() - self.timer.start(Config.TIMER_MS) + + self.plLabel = QLabel(f"Playlist: {self.playlist.playlist_name}") + self.statusbar.addPermanentWidget(self.plLabel) + + self.timer.start(Config.TIMER_MS) def __del__(self): record = Settings.get_int("mainwindow_height") @@ -90,6 +93,8 @@ class Window(QMainWindow, Ui_MainWindow): def connect_signals_slots(self): self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionFade.triggered.connect(self.fade) + self.actionNewPlaylist.triggered.connect(self.create_playlist) + self.actionSelectPlaylist.triggered.connect(self.select_playlist) self.actionPlay_next.triggered.connect(self.play_next) self.actionSearch_database.triggered.connect(self.search_database) self.actionSkip_next.triggered.connect(self.play_next) @@ -102,6 +107,17 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.timeout.connect(self.tick) + def create_playlist(self): + "Create new playlist" + + dlg = QInputDialog(self) + dlg.setInputMode(QInputDialog.TextInput) + dlg.setLabelText("Playlist name:") + dlg.resize(500, 100) + ok = dlg.exec() + if ok: + self.playlist.create_playlist(dlg.textValue()) + def disable_play_next_controls(self): self.actionPlay_next.setEnabled(False) @@ -143,8 +159,10 @@ class Window(QMainWindow, Ui_MainWindow): pass def search_database(self): - dlg = DbDialog(self) - dlg.exec() + self.playlist.search_database() + + def select_playlist(self): + self.playlist.select_playlist() def set_next_track(self): "Set selected track as next" @@ -237,62 +255,8 @@ class Window(QMainWindow, Ui_MainWindow): f"{self.playlist.get_next_artist()}" ) - -class AddNoteDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_dlgAddNote() - self.ui.setupUi(self) - - -class DbDialog(QDialog): - def __init__(self, parent=None): - super().__init__(parent) - self.ui = Ui_Dialog() - self.ui.setupUi(self) - self.ui.searchString.textEdited.connect(self.chars_typed) - self.ui.matchList.itemDoubleClicked.connect(self.listdclick) - - record = Settings.get_int("dbdialog_width") - width = record.f_int or 800 - record = Settings.get_int("dbdialog_height") - height = record.f_int or 600 - self.resize(width, height) - - def __del__(self): - record = Settings.get_int("dbdialog_height") - if record.f_int != self.height(): - record.update({'f_int': self.height()}) - - record = Settings.get_int("dbdialog_width") - if record.f_int != self.width(): - record.update({'f_int': self.width()}) - - def chars_typed(self, s): - if len(s) >= 3: - matches = Tracks.search_titles(s) - self.ui.matchList.clear() - if matches: - for track in matches: - t = QListWidgetItem() - t.setText( - f"{track.title} - {track.artist} " - f"[{helpers.ms_to_mmss(track.duration)}]" - ) - t.setData(Qt.UserRole, track.id) - self.ui.matchList.addItem(t) - - def listdclick(self, entry): - track_id = entry.data(Qt.UserRole) - track = Tracks.track_from_id(track_id) - - # Store in current playlist in database - db_playlist = Playlists.get_only_playlist() - # TODO: hack position to be at end of list - db_playlist.add_track(track, 99999) - - # Add to on-screen playlist - self.parent().add_to_playlist(track) + def update_statusbar(self): + pass def main(): diff --git a/app/playlists.py b/app/playlists.py index 2f2d5f0..de455e8 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -5,7 +5,9 @@ from PyQt5.QtGui import QColor, QDropEvent from PyQt5.QtWidgets import ( QAbstractItemView, QApplication, + QDialog, QHBoxLayout, + QListWidgetItem, QMenu, QMessageBox, QTableWidget, @@ -19,6 +21,8 @@ from config import Config from datetime import datetime, timedelta from log import DEBUG, ERROR from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks +from ui.dlg_search_database_ui import Ui_Dialog +from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist class Playlist(QTableWidget): @@ -199,6 +203,12 @@ class Playlist(QTableWidget): if repaint: self.repaint() + def create_playlist(self, name): + "Create new playlist" + + new_id = Playlists.new(name) + self.load_playlist(new_id) + def drop_on(self, event): index = self.indexAt(event.pos()) if not index.isValid(): @@ -327,21 +337,21 @@ class Playlist(QTableWidget): and pos.y() >= rect.center().y() # noqa W503 ) - def load_playlist(self, name): + def load_playlist(self, plid): """ - Load tracks and notes from named playlist. + Load tracks and notes from playlist id. Set first track as next track to play. Return True if successful else False. """ - DEBUG(f"load_playlist({name})") + DEBUG(f"load_playlist({plid})") - self.playlist_name = name - - p = Playlists.get_playlist_by_name(name) - self.playlist_id = p.id + p = Playlists.get_playlist_by_id(plid) + self.playlist_id = plid + self.playlist_name = p.name + self.parent().parent().update_statusbar() # We need to retrieve playlist tracks and playlist notes, then # add them in row order. We don't mandate that an item will be @@ -354,6 +364,9 @@ class Playlist(QTableWidget): for n in p.notes: data.append(([n.row], n)) + # Clear playlist + self.setRowCount(0) + # Now add data in row order for item in sorted(data, key=lambda x: x[0]): DEBUG(f"Adding {item}") @@ -516,6 +529,14 @@ class Playlist(QTableWidget): # Update display self.tracks_changed() + def search_database(self): + dlg = DbDialog(self) + dlg.exec() + + def select_playlist(self): + dlg = SelectPlaylistDialog(self) + dlg.exec() + def set_selected_as_next(self): """ Sets the selected track as the next track. @@ -647,7 +668,7 @@ class Playlist(QTableWidget): # Get tracks and notes from database db_tracks = {} db_notes = {} - p = Playlists.get_playlist_by_name(self.playlist_name) + p = Playlists.get_playlist_by_id(self.playlist_id) for track in p.tracks: db_tracks[track.track_id] = track.row @@ -682,7 +703,8 @@ class Playlist(QTableWidget): if tracks[id] != db_tracks[id]: DEBUG(f"Update db track.id={id} in database") PlaylistTracks.update_track_row( - self.playlist_id, id, row=tracks[id] + self.playlist_id, track_id=id, + old_row=db_tracks[id], new_row=tracks[id] ) def set_row_bold(self, row): @@ -749,6 +771,76 @@ class Playlist(QTableWidget): self.repaint() +class DbDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_Dialog() + self.ui.setupUi(self) + self.ui.searchString.textEdited.connect(self.chars_typed) + self.ui.matchList.itemDoubleClicked.connect(self.listdclick) + + record = Settings.get_int("dbdialog_width") + width = record.f_int or 800 + record = Settings.get_int("dbdialog_height") + height = record.f_int or 600 + self.resize(width, height) + + def __del__(self): + record = Settings.get_int("dbdialog_height") + if record.f_int != self.height(): + record.update({'f_int': self.height()}) + + record = Settings.get_int("dbdialog_width") + if record.f_int != self.width(): + record.update({'f_int': self.width()}) + + def chars_typed(self, s): + if len(s) >= 3: + matches = Tracks.search_titles(s) + self.ui.matchList.clear() + if matches: + for track in matches: + t = QListWidgetItem() + t.setText( + f"{track.title} - {track.artist} " + f"[{helpers.ms_to_mmss(track.duration)}]" + ) + t.setData(Qt.UserRole, track.id) + self.ui.matchList.addItem(t) + + def listdclick(self, entry): + track_id = entry.data(Qt.UserRole) + track = Tracks.track_from_id(track_id) + + # Store in current playlist in database + db_playlist = Playlists.get_playlist_by_id(self.parent().playlist_id) + # TODO: hack position to be at end of list + db_playlist.add_track(track, 99999) + + # Add to on-screen playlist + self.parent().add_to_playlist(track) + + +class SelectPlaylistDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.ui = Ui_dlgSelectPlaylist() + self.ui.setupUi(self) + self.ui.lstPlaylists.itemDoubleClicked.connect(self.listdclick) + + for (plid, plname) in [ + (a.id, a.name) for a in Playlists.get_all_playlists() + ]: + p = QListWidgetItem() + p.setText(plname) + p.setData(Qt.UserRole, plid) + self.ui.lstPlaylists.addItem(p) + + def listdclick(self, entry): + plid = entry.data(Qt.UserRole) + self.parent().load_playlist(plid) + + class Window(QWidget): def __init__(self): super(Window, self).__init__() diff --git a/app/ui/dlg_SelectPlaylist.ui b/app/ui/dlg_SelectPlaylist.ui new file mode 100644 index 0000000..f11061e --- /dev/null +++ b/app/ui/dlg_SelectPlaylist.ui @@ -0,0 +1,67 @@ + + + dlgSelectPlaylist + + + + 0 + 0 + 276 + 150 + + + + Dialog + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + dlgSelectPlaylist + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + dlgSelectPlaylist + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 746ba9e..0bb8f50 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -6,7 +6,7 @@ 0 0 - 768 + 931 600 @@ -710,7 +710,7 @@ border: 1px solid rgb(85, 87, 83); 0 0 - 768 + 931 18 @@ -724,19 +724,26 @@ border: 1px solid rgb(85, 87, 83); Pla&ylist - - + + + + + + + &Tracks + + + - - + @@ -833,6 +840,16 @@ border: 1px solid rgb(85, 87, 83); &Test + + + Select &playlist + + + + + &New playlist + +