Refactoring mostly done; manage playlist metadata

This commit is contained in:
Keith Edmunds 2021-04-04 12:57:43 +01:00
parent bcfd076a93
commit dadd251587
4 changed files with 227 additions and 106 deletions

View File

@ -10,6 +10,7 @@ class Config(object):
COLOUR_EVEN_PLAYLIST = "#d9d9d9" COLOUR_EVEN_PLAYLIST = "#d9d9d9"
COLOUR_NEXT_HEADER = "#fff3cd" COLOUR_NEXT_HEADER = "#fff3cd"
COLOUR_NEXT_PLAYLIST = "#ffc107" COLOUR_NEXT_PLAYLIST = "#ffc107"
COLOUR_NOTES_PLAYLIST = "#802020"
COLOUR_PREVIOUS_HEADER = "#f8d7da" COLOUR_PREVIOUS_HEADER = "#f8d7da"
DBFS_FADE = -12 DBFS_FADE = -12
DBFS_SILENCE = -50 DBFS_SILENCE = -50
@ -28,7 +29,7 @@ class Config(object):
MILLISECOND_SIGFIGS = 0 MILLISECOND_SIGFIGS = 0
MYSQL_CONNECT = "mysql+mysqldb://songdb:songdb@localhost/songdb" MYSQL_CONNECT = "mysql+mysqldb://songdb:songdb@localhost/songdb"
ROOT = "/home/kae/music" ROOT = "/home/kae/music"
TIMER_MS = 1000 TIMER_MS = 500
config = Config config = Config

View File

@ -1,6 +1,8 @@
#!/usr/bin/python3 #!/usr/bin/python3
import sys import sys
import music
from datetime import datetime, timedelta from datetime import datetime, timedelta
# from log import DEBUG, ERROR # from log import DEBUG, ERROR
@ -27,6 +29,9 @@ class Window(QMainWindow, Ui_MainWindow):
super().__init__(parent) super().__init__(parent)
self.setupUi(self) self.setupUi(self)
self.timer = QTimer() self.timer = QTimer()
self.even_tick = True
self.music = music.Music()
self.playlist.music = self.music
self.connect_signals_slots() self.connect_signals_slots()
self.disable_play_next_controls() self.disable_play_next_controls()
@ -41,9 +46,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.setGeometry(x, y, width, height) self.setGeometry(x, y, width, height)
# Hard code to the only playlist we have for now # Hard code to the only playlist we have for now
self.playlist.load_playlist("Default") if self.playlist.load_playlist("Default"):
self.enable_play_next_controls()
self.timer.start(Config.TIMER_MS) self.timer.start(Config.TIMER_MS)
def __del__(self): def __del__(self):
record = Settings.get_int("mainwindow_height") record = Settings.get_int("mainwindow_height")
@ -83,7 +88,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.action_Clear_selection.triggered.connect(self.clear_selection) self.action_Clear_selection.triggered.connect(self.clear_selection)
self.actionFade.triggered.connect(self.fade) self.actionFade.triggered.connect(self.fade)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionPlay_selected.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.search_database) self.actionSearch_database.triggered.connect(self.search_database)
self.btnPrevious.clicked.connect(self.play_previous) self.btnPrevious.clicked.connect(self.play_previous)
self.btnSearchDatabase.clicked.connect(self.search_database) self.btnSearchDatabase.clicked.connect(self.search_database)
@ -94,37 +98,38 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def disable_play_next_controls(self): def disable_play_next_controls(self):
self.actionPlay_selected.setEnabled(False)
self.actionPlay_next.setEnabled(False) self.actionPlay_next.setEnabled(False)
def enable_play_next_controls(self): def enable_play_next_controls(self):
self.actionPlay_selected.setEnabled(True)
self.actionPlay_next.setEnabled(True) self.actionPlay_next.setEnabled(True)
def fade(self): def fade(self):
self.playlist.fade() self.playlist.fade()
def play_next(self): def play_next(self):
self.music.play_next() self.playlist.play_next()
self.current_track.setText( self.disable_play_next_controls()
f"{self.music.get_current_title()} - "
f"{self.music.get_current_artist()}"
)
self.previous_track.setText( self.previous_track.setText(
f"{self.music.get_previous_title()} - " f"{self.playlist.get_previous_title()} - "
f"{self.music.get_previous_artist()}" f"{self.playlist.get_previous_artist()}"
)
self.current_track.setText(
f"{self.playlist.get_current_title()} - "
f"{self.playlist.get_current_artist()}"
)
self.next_track.setText(
f"{self.playlist.get_next_title()} - "
f"{self.playlist.get_next_artist()}"
) )
self.set_next_track()
# Set time clocks # Set time clocks
now = datetime.now() now = datetime.now()
self.label_start_tod.setText(now.strftime("%H:%M:%S")) self.label_start_tod.setText(now.strftime("%H:%M:%S"))
silence_at = self.music.get_current_silence_at() silence_at = self.playlist.get_current_silence_at()
silence_time = now + timedelta(milliseconds=silence_at) silence_time = now + timedelta(milliseconds=silence_at)
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
self.label_fade_length.setText(helpers.ms_to_mmss( self.label_fade_length.setText(helpers.ms_to_mmss(
silence_at - self.music.get_current_fade_at())) silence_at - self.playlist.get_current_fade_at()))
self.set_playlist_colours()
def play_previous(self): def play_previous(self):
"Resume playing last track" "Resume playing last track"
@ -136,58 +141,54 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = DbDialog(self) dlg = DbDialog(self)
dlg.exec() dlg.exec()
def set_next_track(self):
# TODO
pass
def tick(self): def tick(self):
pass """
# self.current_time.setText(now.strftime("%H:%M:%S")) Update screen
# if self.music.playing():
# playtime = self.music.get_current_playtime()
# self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
# self.label_fade_timer.setText(
# helpers.ms_to_mmss(self.music.get_current_fade_at() - playtime)
# )
# self.label_silent_timer.setText(
# helpers.ms_to_mmss(
# self.music.get_current_silence_at() - playtime))
# self.label_end_timer.setText(
# helpers.ms_to_mmss(
# self.music.get_current_duration() - playtime))
# else:
# # When music ends, ensure next track is selected
# if self.playlist.selectionModel().hasSelection():
# row = self.playlist.currentRow()
# track_id = int(self.playlist.item(row, 0).text())
# if track_id == self.music.get_current_track_id():
# # Current track highlighted: select next
# try:
# self.playlist.selectRow(row + 1)
# except AttributeError:
# # TODO
# pass
The Time of Day clock is updated every tick (500ms).
def update_tod_clock(self): All other timers are updated every second. As the timers have a
"Update time of day clock" one-second resolution, updating every 500ms can result in some
timers updating and then, 500ms later, other timers updating. That
looks odd.
"""
# TODO now = datetime.now()
pass
def update_track_headers(self): self.lblTOD.setText(now.strftime("%H:%M:%S"))
"Update last/current/next track header"
# TODO self.even_tick = not self.even_tick
pass if not self.even_tick:
return
def update_track_clocks(self): if self.music.playing():
"Update started at, silent at, clock times" playtime = self.music.get_playtime()
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
self.label_fade_timer.setText(
helpers.ms_to_mmss(
self.playlist.get_current_fade_at() - playtime)
)
time_to_silence = (
self.playlist.get_current_silence_at() - playtime
)
if time_to_silence < 500:
self.label_silent_timer.setText("00:00")
self.enable_play_next_controls()
else:
self.label_silent_timer.setText(
helpers.ms_to_mmss(time_to_silence)
)
self.label_end_timer.setText(
helpers.ms_to_mmss(
self.playlist.get_current_duration() - playtime))
else:
# When music ends, update playlist display
self.playlist.update_playlist_colours()
# TODO
pass
def update_track_timers(self):
"Update elapsed time, etc, timers"
# TODO
pass
class DbDialog(QDialog): class DbDialog(QDialog):
def __init__(self, parent=None): def __init__(self, parent=None):

View File

@ -10,11 +10,10 @@ from PyQt5.QtWidgets import (
) )
import helpers import helpers
import music
from config import Config from config import Config
from log import DEBUG from log import DEBUG, ERROR
from model import Playlists, Settings, Tracks from model import Playdates, Playlists, Settings, Tracks
class Playlist(QTableWidget): class Playlist(QTableWidget):
@ -27,11 +26,6 @@ class Playlist(QTableWidget):
COL_ENDTIME = 5 COL_ENDTIME = 5
COL_PATH = 6 COL_PATH = 6
# UserRoles
NEXT_TRACK = 1
CURRENT_TRACK = 2
COMMENT = 3
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -45,8 +39,6 @@ class Playlist(QTableWidget):
self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setDragDropMode(QAbstractItemView.InternalMove) self.setDragDropMode(QAbstractItemView.InternalMove)
self.music = music.Music()
self.current_track = None self.current_track = None
self.next_track = None self.next_track = None
self.previous_track_path = None self.previous_track_path = None
@ -136,9 +128,13 @@ class Playlist(QTableWidget):
self.item(drop_row + row_index, self.item(drop_row + row_index,
column_index).setSelected(True) column_index).setSelected(True)
super().dropEvent(event) super().dropEvent(event)
DEBUG(f"Moved row(s) {rows} to become row {drop_row}") DEBUG(f"Moved row(s) {rows} to become row {drop_row}")
def face(self): self.clearSelection()
self.update_playlist_colours()
def fade(self):
self.music.fade() self.music.fade()
def get_current_artist(self): def get_current_artist(self):
@ -220,14 +216,100 @@ class Playlist(QTableWidget):
) )
def load_playlist(self, name): def load_playlist(self, name):
"Load tracks from named playlist" """
Load tracks from named playlist.
Set first track as next track to play.
Return True if successful else False.
"""
for track in Playlists.get_playlist_by_name(name).get_tracks(): for track in Playlists.get_playlist_by_name(name).get_tracks():
self.add_to_playlist(track) self.add_to_playlist(track)
# Set the first playable track as next to play # Set the first playable track as next to play
for row in range(self.rowCount()): for row in range(self.rowCount()):
if self.set_row_as_next_track(row): if self.set_row_as_next_track(row):
break self.meta_set_next(row)
return True
self.update_playlist_colours()
return False
def meta_clear(self, row):
"Clear metad ata for row"
self.meta_set(row, None)
def meta_find(self, metadata, one=True):
"""
Search rows for metadata.
If one is True, check that only one row matches and return
the row number.
If one is False, return a list of matching row numbers.
"""
matches = []
for row in range(self.rowCount()):
if self.meta_get(row) == metadata:
matches.append(row)
if not one:
return matches
if len(matches) == 0:
return None
elif len(matches) == 1:
return matches[0]
else:
ERROR(
f"Multiple matches for metadata '{metadata}' found "
f"in rows: {', '.join([str(x) for x in matches])}"
)
raise AttributeError(f"Multiple '{metadata}' metadata {matches}")
def meta_get(self, row):
"Return row metadata"
return self.item(row, self.COL_INDEX).data(Qt.UserRole)
def meta_get_current(self):
"Return row marked as current, or None"
return self.meta_find("current")
def meta_get_next(self):
"Return row marked as next, or None"
return self.meta_find("next")
def meta_get_notes(self):
"Return rows marked as notes, or None"
return self.meta_find("note", one=False)
def meta_set_current(self, row):
"Mark row as current track"
self.meta_set(row, "current")
def meta_set_next(self, row):
"Mark row as next track"
self.meta_set(row, "next")
def meta_set_note(self, row):
"Mark row as note"
self.meta_set(row, "note")
def meta_set(self, row, metadata):
"Set row metadata"
self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata)
def play_next(self): def play_next(self):
""" """
@ -236,34 +318,68 @@ class Playlist(QTableWidget):
If there is no next track set, return. If there is no next track set, return.
If there's currently a track playing, fade it. If there's currently a track playing, fade it.
Move next track to current track. Move next track to current track.
Cue up next track in playlist if there is one.
Play (new) current. Play (new) current.
Update playlist "current track" metadata
Cue up next track in playlist if there is one.
Update playlist "next track" metadata
Tell database to record it as played Tell database to record it as played
Remember it was played this session Remember it was played for this session
Update playlist appearance. Update playlist appearance.
""" """
# If there is no next track set, return.
if not self.next_track: if not self.next_track:
return return
# If there's currently a track playing, fade it.
if self.music.playing(): if self.music.playing():
path, position = self.music.fade() path, position = self.music.fade()
self.previous_track_path = path self.previous_track_path = path
self.previous_track_position = position self.previous_track_position = position
# Move next track to current track.
self.current_track = self.next_track self.current_track = self.next_track
# Play (new) current.
self.music.play(self.current_track.path) self.music.play(self.current_track.path)
# Find the playlist row for current track # Update playlist "current track" metadata
current_track_id = self.current_track.track_id old_current = self.meta_get_current()
self.current_track_row = None if old_current is not None:
for row in range(self.rowCount): self.meta_clear(old_current)
if self.item(row, 0): current = self.meta_get_next()
if current_track_id == int(self.item(row, 0).text()): if current is not None:
self.current_track_row = row self.meta_set_current(current)
break
self.next_track = None # Cue up next track in playlist if there is one.
track_id = 0
next_row = 0
if current is not None:
start = current + 1
else:
start = 0
for row in range(start, self.rowCount()):
if self.item(row, self.COL_INDEX):
if int(self.item(row, self.COL_INDEX).text()) > 0:
track_id = int(self.item(row, self.COL_INDEX).text())
next_row = row
break
if track_id:
self.next_track = Tracks.get_track(track_id)
# Update playlist "next track" metadata
if next_row:
self.meta_set_next(next_row)
# 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.played_tracks.append(self.current_track.id)
# Update playlist appearance.
self.update_playlist_colours()
def set_next_track(self): def set_next_track(self):
""" """
@ -302,30 +418,33 @@ class Playlist(QTableWidget):
DEBUG(f"set_row_as_next_track: track_id={track_id}") DEBUG(f"set_row_as_next_track: track_id={track_id}")
self.next_track = Tracks.get_track(track_id) self.next_track = Tracks.get_track(track_id)
# Mark row as next track # Mark row as next track
self.item(row, self.COL_INDEX).setData( self.meta_set_next(row)
Qt.UserRole, self.NEXT_TRACK)
self.set_playlist_colours()
return True return True
return False return False
def set_playlist_colours(self): def update_playlist_colours(self):
self.clearSelection() self.clearSelection()
current = self.meta_get_current()
next = self.meta_get_next()
notes = self.meta_get_notes()
for row in range(self.rowCount()): for row in range(self.rowCount()):
if self.item(row, self.COL_INDEX): if row == current:
data = self.item(row, self.COL_INDEX).data(Qt.UserRole) self.set_row_colour(
if data == self.NEXT_TRACK: row, QColor(Config.COLOUR_CURRENT_PLAYLIST))
self.set_row_colour( elif row == next:
row, QColor(Config.COLOUR_NEXT_PLAYLIST)) self.set_row_colour(
elif data == self.CURRENT_TRACK: row, QColor(Config.COLOUR_NEXT_PLAYLIST))
self.set_row_colour( elif row in notes:
row, QColor(Config.COLOUR_CURRENT_PLAYLIST)) self.set_row_colour(
row, QColor(Config.COLOUR_NOTES_PLAYLIST))
else:
if row % 2:
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
else: else:
if row % 2: colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
colour = QColor(Config.COLOUR_ODD_PLAYLIST) self.set_row_colour(row, colour)
else:
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
self.set_row_colour(row, colour)
def set_row_colour(self, row, colour): def set_row_colour(self, row, colour):
for j in range(self.columnCount()): for j in range(self.columnCount()):

View File

@ -723,8 +723,8 @@ border: 1px solid rgb(85, 87, 83);</string>
<property name="title"> <property name="title">
<string>Pla&amp;ylist</string> <string>Pla&amp;ylist</string>
</property> </property>
<addaction name="actionPlay_selected"/>
<addaction name="actionPlay_next"/> <addaction name="actionPlay_next"/>
<addaction name="actionSkip_next"/>
<addaction name="actionSearch_database"/> <addaction name="actionSearch_database"/>
<addaction name="actionAdd_file"/> <addaction name="actionAdd_file"/>
<addaction name="separator"/> <addaction name="separator"/>
@ -745,7 +745,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<string notr="true">background-color: rgb(211, 215, 207);</string> <string notr="true">background-color: rgb(211, 215, 207);</string>
</property> </property>
</widget> </widget>
<action name="actionPlay_selected"> <action name="actionPlay_next">
<property name="icon"> <property name="icon">
<iconset> <iconset>
<normaloff>icon-play.png</normaloff>icon-play.png</iconset> <normaloff>icon-play.png</normaloff>icon-play.png</iconset>
@ -757,7 +757,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>Return</string> <string>Return</string>
</property> </property>
</action> </action>
<action name="actionPlay_next"> <action name="actionSkip_next">
<property name="icon"> <property name="icon">
<iconset> <iconset>
<normaloff>icon-play-next.png</normaloff>icon-play-next.png</iconset> <normaloff>icon-play-next.png</normaloff>icon-play-next.png</iconset>