Add database reference to playlist

Also lots of cleaning up so that headers and playlist track colours
correct follow which playlist has the current/next track.
This commit is contained in:
Keith Edmunds 2021-05-16 12:39:19 +01:00
parent ca9d0c75f8
commit 51cc3bfbca
4 changed files with 219 additions and 199 deletions

View File

@ -169,11 +169,11 @@ class Playlists(Base):
@staticmethod
def new(name):
DEBUG(f"Playlists.new(name={name})")
pl = Playlists()
pl.name = name
session.add(pl)
playlist = Playlists()
playlist.name = name
session.add(playlist)
session.commit()
return pl.id
return playlist
@staticmethod
def open(plid):
@ -184,11 +184,10 @@ class Playlists(Base):
p.last_used = datetime.now()
session.commit()
def close(plid):
def close(self):
"Record playlist as no longer loaded"
p = session.query(Playlists).filter(Playlists.id == plid).one()
p.loaded = False
self.loaded = False
session.commit()
@staticmethod

View File

@ -26,8 +26,6 @@ class Music:
"""
Fade the currently playing track.
Return the current track position.
The actual management of fading runs in its own thread so as not
to hold up the UI during the fade.
"""
@ -39,13 +37,9 @@ class Music:
self.fading = True
position = self.player.get_position()
thread = threading.Thread(target=self._fade)
thread.start()
return position
def _fade(self):
"""
Implementation of fading the current track in a separate thread.
@ -74,7 +68,7 @@ class Music:
p.audio_set_volume(int(self.max_volume * volume_factor))
sleep(sleep_time)
p.pause()
p.stop()
p.release()
self.fading = False
@ -83,6 +77,11 @@ class Music:
return self.player.get_time()
def get_position(self):
"Return current position"
return self.player.get_position()
def play(self, path):
"""
Start playing the track at path.

View File

@ -46,7 +46,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.music = music.Music()
self.current_track = None
self.current_track_playlist = None
self.next_track = None
self.next_track_playlist = None
self.previous_track = None
self.previous_track_position = None
self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT)
@ -54,7 +56,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.menuTest.menuAction().setVisible(Config.TESTMODE)
self.set_main_window_size()
self.current_playlist = self.tabPlaylist.currentWidget
self.visible_playlist = self.tabPlaylist.currentWidget
self.load_last_playlists()
self.enable_play_next_controls()
@ -70,7 +72,7 @@ class Window(QMainWindow, Ui_MainWindow):
if dlg.exec_():
for fname in dlg.selectedFiles():
track = add_path_to_db(fname)
self.current_playlist().add_to_playlist(track)
self.visible_playlist()._add_to_playlist(track)
def set_main_window_size(self):
@ -85,8 +87,8 @@ class Window(QMainWindow, Ui_MainWindow):
self.setGeometry(x, y, width, height)
def clear_selection(self):
if self.current_playlist():
self.current_playlist().clearSelection()
if self.visible_playlist():
self.visible_playlist().clearSelection()
def closeEvent(self, event):
"Don't allow window to close when a track is playing"
@ -152,7 +154,8 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
self.current_playlist().create_playlist(dlg.textValue())
playlist = Playlists.new(dlg.textValue())
self.load_playlist(playlist)
def change_volume(self, volume):
"Change player maximum volume"
@ -162,7 +165,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.music.set_volume(volume)
def close_playlist(self):
Playlists.close(self.current_playlist().id)
self.visible_playlist().db.close()
index = self.tabPlaylist.currentIndex()
self.tabPlaylist.removeTab(index)
@ -192,22 +195,25 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
self.current_playlist().add_note(dlg.textValue())
self.visible_playlist().add_note(dlg.textValue())
def load_last_playlists(self):
"Load the playlists that we loaded at end of last session"
for p in Playlists.get_last_used():
playlist = Playlist()
playlist.load_playlist(p.id)
last_tab = self.tabPlaylist.addTab(playlist, p.name)
for playlist in Playlists.get_last_used():
DEBUG(f"load_last_playlists(), {playlist.name=}, {playlist.id=}")
self.load_playlist(playlist)
# Set last tab as active
self.tabPlaylist.setCurrentIndex(last_tab)
# Get next track
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
def load_playlist(self, playlist_db):
"""
Take the passed database object, create a playlist display, attach
the database object, get it populated and then add tab.
"""
playlist_table = Playlist()
playlist_table.db = playlist_db
playlist_table.populate()
self.tabPlaylist.addTab(playlist_table, playlist_db.name)
def play_next(self):
"""
@ -235,38 +241,34 @@ class Window(QMainWindow, Ui_MainWindow):
f"{self.current_track.title if self.current_track else None}"
)
# If there's currently a track playing, fade it.
if self.music.playing():
self.previous_track_position = self.music.fade()
self.previous_track = self.current_track
# Stop current track, if any
self.stop_playing()
# Shuffle tracks along
# Play next track
self.current_track = self.next_track
self.current_track_playlist = self.next_track_playlist
self.next_track = None
# Play (new) current.
self.next_track_playlist = None
self.music.play(self.current_track.path)
# Update metadata
next_track_id = self.current_playlist().started_playing_next()
next_track_id = self.current_track_playlist.play_started()
if next_track_id is not None:
self.next_track = Tracks.get_track(next_track_id)
self.next_track_playlist = self.current_track_playlist
# Check we can read it
if not os.access(self.next_track.path, os.R_OK):
self.show_warning(
"Can't read next track",
self.next_track.path)
else:
self.next_track = None
self.next_track = self.next_track_playlist = None
# 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.current_playlist().mark_track_played(self.current_track.id)
self.disable_play_next_controls()
self.update_headers()
@ -286,32 +288,25 @@ class Window(QMainWindow, Ui_MainWindow):
# TODO
pass
def playlist_changed(self):
"The playlist has changed (probably because the user changed tabs)"
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
def search_database(self):
dlg = DbDialog(self)
dlg.exec()
def select_playlist(self):
# TODO don't show those that are currently open
dlg = SelectPlaylistDialog(self)
dlg.exec()
playlist = Playlist()
plid = dlg.plid
dlg.close()
playlist.load_playlist(plid)
new_tab = self.tabPlaylist.addTab(playlist, Playlists.get_name(plid))
self.tabPlaylist.setCurrentIndex(new_tab)
playlist = Playlists.get_playlist_by_id(dlg.plid)
self.load_playlist(playlist)
def set_next_track(self):
"Set selected track as next"
next_track_id = self.current_playlist().set_selected_as_next()
next_track_id = self.visible_playlist().set_selected_as_next()
if next_track_id:
if self.next_track_playlist:
self.next_track_playlist.clear_next()
self.next_track_playlist = self.visible_playlist()
self.next_track = Tracks.get_track(next_track_id)
self.update_headers()
@ -323,7 +318,7 @@ class Window(QMainWindow, Ui_MainWindow):
def songfacts_search(self):
"Open a browser window in Songfacts searching for selected title"
title = self.current_playlist().get_selected_title()
title = self.visible_playlist().get_selected_title()
if title:
slug = slugify(title, replacements=([["'", ""]]))
url = f"https://www.songfacts.com/search/songs/{slug}"
@ -332,15 +327,34 @@ class Window(QMainWindow, Ui_MainWindow):
def stop(self):
"Stop playing immediately"
DEBUG("musicmuster.stop()")
self.stop_playing(fade=False)
def stop_playing(self, fade=True):
"Stop playing current track"
DEBUG("musicmuster.stop_playing()")
if not self.music.playing():
return
self.previous_track_position = self.music.get_position()
if fade:
self.music.fade()
else:
self.music.stop()
self.current_track_playlist.clear_current()
# Shuffle tracks along
self.previous_track = self.current_track
self.previous_track_position = self.music.stop()
self.update_headers()
def tab_change(self):
"User has changed tabs, so refresh next track"
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
pass
def test_function(self):
"Placeholder for test function"
@ -427,11 +441,12 @@ class Window(QMainWindow, Ui_MainWindow):
if self.playing:
self.label_end_timer.setText("00:00")
self.frame_silent.setStyleSheet("")
self.current_track_playlist.play_stopped()
self.playing = False
self.previous_track = self.current_track
self.current_track = None
self.current_track_playlist = None
self.previous_track_position = 0
self.current_playlist().stopped_playing()
self.update_headers()
def update_headers(self):
@ -464,7 +479,7 @@ class Window(QMainWindow, Ui_MainWindow):
def wikipedia_search(self):
"Open a browser window in Wikipedia searching for selected title"
title = self.current_playlist().get_selected_title()
title = self.visible_playlist().get_selected_title()
if title:
str = urllib.parse.quote_plus(title)
url = f"https://www.wikipedia.org/w/index.php?search={str}"
@ -531,7 +546,7 @@ class DbDialog(QDialog):
def add_track(self, track_id):
track = Tracks.track_from_id(track_id)
self.parent().current_playlist().add_to_playlist(track)
self.parent().visible_playlist()._add_to_playlist(track)
# Select search text to make it easier for next search
self.select_searchtext()

View File

@ -17,7 +17,7 @@ import os
from config import Config
from datetime import datetime, timedelta
from log import DEBUG, ERROR
from model import Notes, Playlists, PlaylistTracks, Settings, Tracks
from model import Notes, PlaylistTracks, Settings, Tracks
class Playlist(QTableWidget):
@ -80,7 +80,6 @@ class Playlist(QTableWidget):
self.customContextMenuRequested.connect(self._context_menu)
self.viewport().installEventFilter(self)
self.id = None
self.current_track_start_time = None
self.played_tracks = []
@ -138,7 +137,6 @@ class Playlist(QTableWidget):
f"Moved row(s) {rows} to become row {drop_row}"
)
self._save_playlist()
self._repaint()
def eventFilter(self, source, event):
@ -163,7 +161,7 @@ class Playlist(QTableWidget):
# ########## Externally called functions ##########
def add_note(self, text):
def add_note(self, note, repaint=True):
"""
Add note to playlist
@ -177,89 +175,93 @@ class Playlist(QTableWidget):
row = self.rowCount()
DEBUG(f"playlist.add_note(): row={row}")
note = Notes.add_note(self.id, row, text)
self.add_to_playlist(note, row=row)
def add_to_playlist(self, data, repaint=True, row=None):
"""
Add data to playlist. Data may be either a Tracks object or a
Notes object.
"""
DEBUG(f"add_to_playlist(data={data}, repaint={repaint}, row={row}")
if not row:
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"add_to_playlist: row set to {row}")
# Does note end with a time?
start_time = None
try:
start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time()
DEBUG(f"Note contains valid time={start_time}")
except ValueError:
DEBUG(
f"Note on row {row} ('{note.note}') "
"does not contain valid time"
)
self.insertRow(row)
if isinstance(data, Tracks):
DEBUG(f"add_to_playlist: track.id={data.id}")
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)
titleitem = QTableWidgetItem(track.title)
self.setItem(row, self.COL_TITLE, titleitem)
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)
elif isinstance(data, Notes):
DEBUG(f"add_to_playlist: note.id={data.id}")
note = data
item = QTableWidgetItem(str(note.id))
self.setItem(row, self.COL_INDEX, item)
titleitem = QTableWidgetItem(note.note)
self.setItem(row, self.COL_NOTE, titleitem)
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
# Does note end with a time?
start_time = None
try:
start_time = datetime.strptime(
note.note[-9:], " %H:%M:%S").time()
DEBUG(f"Note contains valid time={start_time}")
except ValueError:
DEBUG(
f"Note on row {row} ('{note.note}') "
"does not contain valid time"
)
# Add start times or empty items as background
# colour won't be set for columns without items
self._set_row_time(row, start_time)
item = QTableWidgetItem()
self.setItem(row, self.COL_PATH, item)
item = QTableWidgetItem(str(note.id))
self.setItem(row, self.COL_INDEX, item)
titleitem = QTableWidgetItem(data.note)
self.setItem(row, self.COL_NOTE, titleitem)
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
self.NOTE_COL_SPAN)
# Add start times or empty items as background
# colour won't be set for columns without items
self._set_row_time(row, start_time)
item = QTableWidgetItem()
self.setItem(row, self.COL_PATH, item)
self._meta_set_note(row)
else:
ERROR(f"Unknown data passed to add_to_playlist({data})")
return
self._meta_set_note(row)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self._save_playlist()
self._repaint(clear_selection=False)
def create_playlist(self, name):
"Create new playlist"
def add_track(self, track, repaint=True):
"""
Add track to playlist
new_id = Playlists.new(name)
self.load_playlist(new_id)
If a row is selected, add track above. Otherwise, add to end of
playlist.
"""
if self.selectionModel().hasSelection():
row = self.currentRow()
else:
row = self.rowCount()
DEBUG(f"add_track({track=}), {row=}")
self.insertRow(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)
titleitem = QTableWidgetItem(track.title)
self.setItem(row, self.COL_TITLE, titleitem)
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)
# Add empty start time for now as background
# colour won't be set for columns without items
item = QTableWidgetItem()
self.setItem(row, self.COL_PATH, item)
# Scroll to new row
self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter)
if repaint:
self._repaint(clear_selection=False)
def clear_current(self):
"Clear current track"
self._meta_clear_current()
self._repaint(save_playlist=False)
def clear_next(self):
"Clear next track"
self._meta_clear_next()
self._repaint(save_playlist=False)
def get_next_track_id(self):
"Return next track id"
next_row = self._meta_get_next()
return self._get_row_id(next_row)
@ -273,24 +275,30 @@ class Playlist(QTableWidget):
else:
return None
def load_playlist(self, plid):
def play_started(self):
"""
Load tracks and notes from playlist id.
If this is not the first playlist loaded, we may already have
a next track set in which case don't change it. Otherwise, set
the first non-notes row as next track to play.
Update current track to be what was next, and determine next track.
Return next track_id.
"""
DEBUG(f"load_playlist(plid={plid})")
self.current_track_start_time = datetime.now()
self.id = plid
Playlists.open(plid)
p = Playlists.get_playlist_by_id(plid)
self.db = p
self.populate_playlist()
current_row = self._meta_get_next()
self._meta_set_current(current_row)
self.played_tracks.append(current_row)
def populate_playlist(self):
# Scroll to put current track in centre
scroll_to = self.item(current_row, self.COL_INDEX)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
next_track_id = self._mark_next_track()
self._repaint(save_playlist=False)
return next_track_id
def play_stopped(self):
self._meta_clear_current()
self.current_track_start_time = None
def populate(self):
# 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.
@ -306,15 +314,7 @@ class Playlist(QTableWidget):
# Now add data in row order
for item in sorted(data, key=lambda x: x[0]):
self.add_to_playlist(item[1], repaint=False)
# Set next track for this playlist
notes_rows = self._meta_get_notes()
for row in range(self.rowCount()):
if row in notes_rows:
continue
self._meta_set_next(row)
break
self._add_to_playlist(item[1], repaint=False)
# Scroll to top
scroll_to = self.item(0, self.COL_INDEX)
@ -322,9 +322,6 @@ class Playlist(QTableWidget):
self._repaint()
def mark_track_played(self, track_id):
self.played_tracks.append(track_id)
def set_selected_as_next(self):
"""
Sets the selected track as the next track.
@ -335,26 +332,21 @@ class Playlist(QTableWidget):
return self._set_next(self.currentRow())
def started_playing_next(self):
"""
Update current track to be what was next, and determine next track.
Return next track_id.
"""
self.current_track_start_time = datetime.now()
current_row = self._meta_get_next()
self._meta_set_current(current_row)
# Scroll to put current track in centre
scroll_to = self.item(current_row, self.COL_INDEX)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtCenter)
return self._mark_next_track()
def stopped_playing(self):
self._meta_clear_current()
self.current_track_start_time = None
# ########## Internally called functions ##########
def _add_to_playlist(self, data, repaint=True):
"""
Add data to playlist. Data may be either a Tracks object or a
Notes object.
"""
DEBUG(f"_add_to_playlist({data=})")
if isinstance(data, Tracks):
self.add_track(data, repaint=repaint)
elif isinstance(data, Notes):
self.add_note(data, repaint=repaint)
def _calculate_next_start_time(self, row, start):
"Return this row's end time given its start time"
@ -404,7 +396,7 @@ class Playlist(QTableWidget):
if row in self._meta_get_notes():
Notes.delete_note(id)
else:
PlaylistTracks.remove_track(self.id, row)
PlaylistTracks.remove_track(self.db.id, row)
self.removeRow(row)
self._repaint()
@ -484,8 +476,6 @@ class Playlist(QTableWidget):
if not found_next_track:
return None
self._repaint()
track_id = self._get_row_id(row)
return track_id
@ -501,9 +491,19 @@ class Playlist(QTableWidget):
"""
current_row = self._meta_get_current()
if current_row:
if current_row is not None:
self._meta_clear(current_row)
def _meta_clear_next(self):
"""
Clear next row if there is one. There may not be if
we've changed playlists
"""
next_row = self._meta_get_next()
if next_row is not None:
self._meta_clear(next_row)
def _meta_find(self, metadata, one=True):
"""
Search rows for metadata.
@ -581,7 +581,10 @@ class Playlist(QTableWidget):
title = self.item(row, self.COL_TITLE).text()
else:
title = ""
DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})")
DEBUG(
f"playlist[{self.db.id}:{self.db.name}]._meta_set(row={row}, "
f"title={title}, metadata={metadata})"
)
if row is None:
raise ValueError("_meta_set() with row=None")
@ -600,20 +603,26 @@ class Playlist(QTableWidget):
if self.item(row, self.COL_INDEX):
self._meta_set_next(row)
self._repaint()
self._repaint(save_playlist=False)
return self._get_row_id(row)
else:
return None
def _repaint(self, clear_selection=True):
def _repaint(self, clear_selection=True, save_playlist=True):
"Set row colours, fonts, etc, and save playlist"
DEBUG(f"_repaint(clear_selection={clear_selection})")
DEBUG(
f"playlist[{self.db.id}:{self.db.name}]."
f"_repaint({clear_selection=}, {save_playlist=})"
)
if clear_selection:
self.clearSelection()
if save_playlist:
self._save_playlist()
current = self._meta_get_current()
next = self._meta_get_next() or 0
next = self._meta_get_next()
notes = self._meta_get_notes()
# Set colours and start times
@ -697,8 +706,6 @@ class Playlist(QTableWidget):
times in one playlist and in multiple playlists.
"""
playlist = Playlists.get_playlist_by_id(self.id)
# Notes first
# Create dictionaries indexed by note_id
playlist_notes = {}
@ -714,7 +721,7 @@ class Playlist(QTableWidget):
playlist_notes[note_id] = row
# Database
for note in playlist.notes:
for note in self.db.notes:
database_notes[note.id] = note.row
# Notes to add to database
@ -723,14 +730,14 @@ class Playlist(QTableWidget):
for note_id in set(playlist_notes.keys()) - set(database_notes.keys()):
ERROR(
f"_save_playlist(): Note.id={note_id} "
f"missing from playlist {playlist} in database"
f"missing from playlist {self} in database"
)
# Notes to remove from database
for note_id in set(database_notes.keys()) - set(playlist_notes.keys()):
DEBUG(
f"_save_playlist(): Delete note note_id={note_id} "
f"from playlist {playlist} in database"
f"from playlist {self} in database"
)
Notes.delete_note(note_id)
@ -756,7 +763,7 @@ class Playlist(QTableWidget):
playlist_tracks[row] = self._get_row_id(row)
# Database
for track in playlist.tracks:
for track in self.db.tracks:
database_tracks[track.row] = track.track_id
# Tracks rows to add to database
@ -764,7 +771,7 @@ class Playlist(QTableWidget):
set(set(playlist_tracks.keys()) - set(database_tracks.keys()))
):
DEBUG(f"_save_playlist(): row {row} missing from database")
PlaylistTracks.add_track(self.id, playlist_tracks[row], row)
PlaylistTracks.add_track(self.db.id, playlist_tracks[row], row)
# Track rows to remove from database
for row in (
@ -775,7 +782,7 @@ class Playlist(QTableWidget):
f"_save_playlist(): row {row} in database not playlist "
f"(track={track})"
)
PlaylistTracks.remove_track(playlist.id, row)
PlaylistTracks.remove_track(self.db.id, row)
# Track rows to update in database
for row in (
@ -784,11 +791,11 @@ class Playlist(QTableWidget):
if playlist_tracks[row] != database_tracks[row]:
DEBUG(
"_save_playlist(): Update row={row} in database for "
f"playlist {playlist} from track={database_tracks[row]} "
f"playlist {self} from track={database_tracks[row]} "
f"to track={playlist_tracks[row]}"
)
PlaylistTracks.update_row_track(
self.id, row, playlist_tracks[row])
self.db.id, row, playlist_tracks[row])
def _set_column_widths(self):