diff --git a/app/model.py b/app/model.py index 0cea47b..91b848a 100644 --- a/app/model.py +++ b/app/model.py @@ -25,7 +25,8 @@ from log import DEBUG, ERROR, INFO INFO("Connect to database") engine = sqlalchemy.create_engine(f"{Config.MYSQL_CONNECT}?charset=utf8", encoding='utf-8', - echo=Config.DISPLAY_SQL) + echo=Config.DISPLAY_SQL, + pool_pre_ping=True) Base = declarative_base() Base.metadata.create_all(engine) @@ -35,6 +36,159 @@ session = Session() # Database classes +class Notes(Base): + __tablename__ = 'notes' + + id = Column(Integer, primary_key=True, autoincrement=True) + playlist_id = Column(Integer, ForeignKey('playlists.id')) + playlist = relationship("Playlists", back_populates="notes") + row = Column(Integer, nullable=False) + note = Column(String(256), index=False) + + def __repr__(self): + return ( + f"" + ) + + @staticmethod + def add_note(playlist_id, row, text): + DEBUG(f"add_note({text})") + note = Notes() + note.playlist_id = playlist_id + note.row = row + note.note = text + session.add(note) + session.commit() + return note.id + + @classmethod + def update_note(cls, id, row, text): + DEBUG(f"update_note({id}, {row}, {text})") + + note = session.query(cls).filter(cls.id == id).one() + note.row = row + note.note = text + session.commit() + + +class Playdates(Base): + __tablename__ = 'playdates' + + id = Column(Integer, primary_key=True, autoincrement=True) + lastplayed = Column(DateTime, index=True, default=None) + track_id = Column(Integer, ForeignKey('tracks.id')) + tracks = relationship("Tracks", back_populates="playdates") + + @staticmethod + def add_playdate(track): + DEBUG(f"add_playdate({track})") + pd = Playdates() + pd.lastplayed = datetime.now() + pd.track_id = track.id + session.add(pd) + track.update_lastplayed() + session.commit() + + +class Playlists(Base): + """ + Usage: + + pl = session.query(Playlists).filter(Playlists.id == 1).one() + + pl + + + pl.tracks + [<__main__.PlaylistTracks at 0x7fcd20181c18>, + <__main__.PlaylistTracks at 0x7fcd20181c88>, + <__main__.PlaylistTracks at 0x7fcd20181be0>, + <__main__.PlaylistTracks at 0x7fcd20181c50>] + + [a.tracks for a in pl.tracks] + [ + + glue.track_id = tr.id + + pl.tracks.append(glue) + + session.commit() + + [a.tracks for a in pl.tracks] + [") + + # 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" + + return session.query(Playlists).filter(Playlists.name == name).one() + + def add_track(self, track, row): + glue = PlaylistTracks(row=row) + glue.track_id = track.id + self.tracks.append(glue) + + def get_notes(self): + return [a.note for a in self.notes] + + def get_tracks(self): + return [a.tracks for a in self.tracks] + + +class PlaylistTracks(Base): + __tablename__ = 'playlisttracks' + + id = Column(Integer, primary_key=True, autoincrement=True) + playlist_id = Column(Integer, ForeignKey('playlists.id'), primary_key=True) + track_id = Column(Integer, ForeignKey('tracks.id'), primary_key=True) + row = Column(Integer, nullable=False) + tracks = relationship("Tracks", back_populates="playlists") + playlists = relationship("Playlists", back_populates="tracks") + + @staticmethod + def update_track_row(playlist_id, track_id, row): + DEBUG(f"update_track({id}, {row})") + + plt = session.query(PlaylistTracks).filter( + PlaylistTracks.playlist_id == playlist_id, + PlaylistTracks.track_id == track_id + ).one() + plt.row = row + session.commit() + + class Settings(Base): __tablename__ = 'settings' @@ -64,88 +218,6 @@ class Settings(Base): session.commit() -class PlaylistTracks(Base): - __tablename__ = 'playlisttracks' - Base.metadata, - id = Column(Integer, primary_key=True, autoincrement=True) - playlist_id = Column(Integer, ForeignKey('playlists.id'), primary_key=True) - track_id = Column(Integer, ForeignKey('tracks.id'), primary_key=True) - sort = Column(Integer, nullable=False) - tracks = relationship("Tracks", back_populates="playlists") - playlists = relationship("Playlists", back_populates="tracks") - - -class Playlists(Base): - """ - Usage: - - pl = session.query(Playlists).filter(Playlists.id == 1).one() - - pl - - - pl.tracks - [<__main__.PlaylistTracks at 0x7fcd20181c18>, - <__main__.PlaylistTracks at 0x7fcd20181c88>, - <__main__.PlaylistTracks at 0x7fcd20181be0>, - <__main__.PlaylistTracks at 0x7fcd20181c50>] - - [a.tracks for a in pl.tracks] - [ - - glue.track_id = tr.id - - pl.tracks.append(glue) - - session.commit() - - [a.tracks for a in pl.tracks] - [") - - # 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" - - return session.query(Playlists).filter(Playlists.name == name).one() - - def add_track(self, track, position): - glue = PlaylistTracks(sort=position) - glue.track_id = track.id - self.tracks.append(glue) - - def get_tracks(self): - return [a.tracks for a in self.tracks] - - class Tracks(Base): __tablename__ = 'tracks' @@ -227,22 +299,3 @@ class Tracks(Base): def update_lastplayed(self): self.lastplayed = datetime.now() - - -class Playdates(Base): - __tablename__ = 'playdates' - - id = Column(Integer, primary_key=True, autoincrement=True) - lastplayed = Column(DateTime, index=True, default=None) - track_id = Column(Integer, ForeignKey('tracks.id')) - tracks = relationship("Tracks", back_populates="playdates") - - @staticmethod - def add_playdate(track): - DEBUG(f"add_playdate({track})") - pd = Playdates() - pd.lastplayed = datetime.now() - pd.track_id = track.id - session.add(pd) - track.update_lastplayed() - session.commit() diff --git a/app/playlists.py b/app/playlists.py index c73d37f..34bea4c 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -15,19 +15,23 @@ import helpers from config import Config from datetime import datetime, timedelta from log import DEBUG, ERROR -from model import Playdates, Playlists, Settings, Tracks +from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks class Playlist(QTableWidget): # Column names COL_INDEX = 0 COL_MSS = 1 + COL_NOTE = 1 COL_TITLE = 2 COL_ARTIST = 3 COL_DURATION = 4 COL_ENDTIME = 5 COL_PATH = 6 + NOTE_COL_SPAN = 6 + NOTE_ROW_SPAN = 1 + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -43,6 +47,8 @@ class Playlist(QTableWidget): self.current_track = None self.next_track = None + self.playlist_name = None + self.playlist_id = 0 self.previous_track = None self.previous_track_position = None self.played_tracks = [] @@ -73,7 +79,7 @@ class Playlist(QTableWidget): if record.f_int != self.columnWidth(column): record.update({'f_int': width}) - def add_note(self, note): + def add_note(self, text): """ Add note to playlist @@ -81,47 +87,62 @@ class Playlist(QTableWidget): playlist. """ - DEBUG(f"add_note({note})") + DEBUG(f"add_note({text})") DEBUG(f"self.currentRow()={self.currentRow()}") row = self.currentRow() if row < 0: row = self.rowCount() DEBUG(f"row={row}") + note_id = Notes.add_note(self.playlist_id, row, text) self.insertRow(row) - item = QTableWidgetItem("0") + item = QTableWidgetItem(str(note_id)) self.setItem(row, self.COL_INDEX, item) - item = QTableWidgetItem(note) - self.setItem(row, 1, item) - self.setSpan(row, 1, 1, 6) + item = QTableWidgetItem(text) + self.setItem(row, self.COL_NOTE, item) + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + self.NOTE_COL_SPAN) self.meta_set_note(row) self.repaint() - def add_to_playlist(self, track): + def add_to_playlist(self, data, repaint=True): """ - Add track to playlist - - track is an instance of Track + Add data to playlist. Data may be either a Tracks object or a + Notes object. """ - DEBUG(f"add_to_playlist: track.id={track.id}") row = self.rowCount() self.insertRow(row) DEBUG(f"Adding to row {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) - item = QTableWidgetItem(track.title) - self.setItem(row, self.COL_TITLE, item) - 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) + if isinstance(data, Tracks): + 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) + item = QTableWidgetItem(track.title) + self.setItem(row, self.COL_TITLE, item) + 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) + DEBUG(f"add_to_playlist: track.id={track.id}") + else: + # This is a note + item = QTableWidgetItem(str(data.id)) + self.setItem(row, self.COL_INDEX, item) + item = QTableWidgetItem(data.note) + self.setItem(row, self.COL_NOTE, item) + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + self.NOTE_COL_SPAN) + self.meta_set_note(row) + + if repaint: + self.repaint() def drop_on(self, event): index = self.indexAt(event.pos()) @@ -253,7 +274,7 @@ class Playlist(QTableWidget): def load_playlist(self, name): """ - Load tracks from named playlist. + Load tracks and notes from named playlist. Set first track as next track to play. @@ -262,15 +283,36 @@ class Playlist(QTableWidget): DEBUG(f"load_playlist({name})") - for track in Playlists.get_playlist_by_name(name).get_tracks(): - self.add_to_playlist(track) + self.playlist_name = name - # Set the first playable track as next to play + p = Playlists.get_playlist_by_name(name) + self.playlist_id = p.id + + # We need to retrieve playlist tracks and playlist notes, then + # 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. + data = [] + + for t in p.tracks: + data.append(([t.row], t.tracks)) + for n in p.notes: + data.append(([n.row], n)) + + # Now add data in row order + DEBUG(data) + for item in sorted(data, key=lambda x: x[0]): + DEBUG(f"Adding {item}") + self.add_to_playlist(item[1], repaint=False) + + # Set the first non-notes row as next track to play + notes_rows = self.meta_get_notes() for row in range(self.rowCount()): - if self.item(row, self.COL_INDEX): - self.meta_set_next(row) - self.tracks_changed() - return True + if row in notes_rows: + continue + self.meta_set_next(row) + self.tracks_changed() + return True return False @@ -450,7 +492,9 @@ class Playlist(QTableWidget): self.tracks_changed() def repaint(self): - "Set row colours, fonts, etc" + "Set row colours, fonts, etc, and save playlist" + + self.save() self.clearSelection() current = self.meta_get_current() @@ -512,6 +556,73 @@ class Playlist(QTableWidget): else: self.set_row_bold(row) + def save(self): + """ + Save playlist to database. + + Notes are also saved. + """ + + # Create list of current tracks (row, track_id) for this playlst and + # compare with actual playlist. Fix as required. + # Repeat for notes (row, id, text) + + tracks = {} + notes = {} + note_rows = self.meta_get_notes() + + for row in range(self.rowCount()): + if self.item(row, self.COL_INDEX): + id = int(self.item(row, self.COL_INDEX).text()) + else: + DEBUG(f"(no COL_INDEX data in row {row}") + continue + if row in note_rows: + notes[id] = (row, self.item(row, self.COL_NOTE).text()) + else: + tracks[id] = row + + # Get tracks and notes from database + db_tracks = {} + db_notes = {} + p = Playlists.get_playlist_by_name(self.playlist_name) + + for track in p.tracks: + db_tracks[track.track_id] = track.row + + for note in p.notes: + db_notes[note.id] = (note.row, note.note) + + # Note ids to remove from db + for id in set(db_notes.keys()) - set(notes.keys()): + DEBUG(f"Delete note.id={id} from database") + + # Note ids to add to db + for id in set(notes.keys()) - set(db_notes.keys()): + DEBUG(f"Add note.id={id} to database") + + # Notes to update in db + for id in set(notes.keys()) & set(db_notes.keys()): + if notes[id] != db_notes[id]: + DEBUG(f"Update db note.id={id} in database") + Notes.update_note(id, row=notes[id][0], text=notes[id][1]) + + # Track ids to remove from db + for id in set(db_tracks.keys()) - set(tracks.keys()): + DEBUG(f"Delete track.id={id} from database") + + # Track ids to add to db + for id in set(tracks.keys()) - set(db_tracks.keys()): + DEBUG(f"Add track.id={id} to database") + + # Tracks to update in db + for id in set(tracks.keys()) & set(db_tracks.keys()): + 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] + ) + def set_row_bold(self, row): bold = QFont() bold.setBold(True) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 97027b9..74f3441 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -719,6 +719,7 @@ border: 1px solid rgb(85, 87, 83); Fi&le + @@ -828,6 +829,11 @@ border: 1px solid rgb(85, 87, 83); E&xit + + + Test + +