Store playlist order; allow reordering and save

This commit is contained in:
Keith Edmunds 2021-04-06 16:40:54 +01:00
parent 765f3d62d6
commit 5a446919e3
3 changed files with 305 additions and 135 deletions

View File

@ -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"<Note(id={self.id}, row={self.row}, note={self.note}>"
)
@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
<Playlist(id=1, name=Default>
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]
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]]
glue = PlaylistTracks(row=5)
tr = session.query(Tracks).filter(Tracks.id == 676).one()
tr
<Track(id=676, title=Seven Nation Army, artist=White Stripes,
path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
glue.track_id = tr.id
pl.tracks.append(glue)
session.commit()
[a.tracks for a in pl.tracks]
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]
<Track(id=676, title=Seven Nation Army, artist=White Stripes[...]]
"""
__tablename__ = "playlists"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True)
notes = relationship("Notes",
order_by="Notes.row",
back_populates="playlist")
tracks = relationship("PlaylistTracks",
order_by="PlaylistTracks.row",
back_populates="playlists")
def __repr__(self):
return (f"<Playlist(id={self.id}, name={self.name}>")
# 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
<Playlist(id=1, name=Default>
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]
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]]
glue = PlaylistTracks(sort=5)
tr = session.query(Tracks).filter(Tracks.id == 676).one()
tr
<Track(id=676, title=Seven Nation Army, artist=White Stripes,
path=/home/kae/music/White Stripes/Elephant/01. Seven Nation Army.flac>
glue.track_id = tr.id
pl.tracks.append(glue)
session.commit()
[a.tracks for a in pl.tracks]
[<Track(id=3992, title=Yesterday Man, artist=Various, path=/h[...]
<Track(id=2238, title=These Boots Are Made For Walkin', arti[...]
<Track(id=3837, title=Babe, artist=Various, path=/home/kae/m[...]
<Track(id=2332, title=Such Great Heights - Remastered, artis[...]
<Track(id=676, title=Seven Nation Army, artist=White Stripes[...]]
"""
__tablename__ = "playlists"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True)
tracks = relationship("PlaylistTracks",
order_by="PlaylistTracks.sort",
back_populates="playlists")
def __repr__(self):
return (f"<Playlist(id={self.id}, name={self.name}>")
# 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()

View File

@ -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)

View File

@ -719,6 +719,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>Fi&amp;le</string>
</property>
<addaction name="actionE_xit"/>
<addaction name="actionTest"/>
</widget>
<widget class="QMenu" name="menuPlaylist">
<property name="title">
@ -828,6 +829,11 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>E&amp;xit</string>
</property>
</action>
<action name="actionTest">
<property name="text">
<string>Test</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>