Store playlist order; allow reordering and save
This commit is contained in:
parent
765f3d62d6
commit
5a446919e3
257
app/model.py
257
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"<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()
|
||||
|
||||
177
app/playlists.py
177
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)
|
||||
|
||||
@ -719,6 +719,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
||||
<string>Fi&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&xit</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionTest">
|
||||
<property name="text">
|
||||
<string>Test</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user