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_NEXT_HEADER = "#fff3cd"
COLOUR_NEXT_PLAYLIST = "#ffc107"
COLOUR_NOTES_PLAYLIST = "#802020"
COLOUR_PREVIOUS_HEADER = "#f8d7da"
DBFS_FADE = -12
DBFS_SILENCE = -50
@ -28,7 +29,7 @@ class Config(object):
MILLISECOND_SIGFIGS = 0
MYSQL_CONNECT = "mysql+mysqldb://songdb:songdb@localhost/songdb"
ROOT = "/home/kae/music"
TIMER_MS = 1000
TIMER_MS = 500
config = Config

View File

@ -1,6 +1,8 @@
#!/usr/bin/python3
import sys
import music
from datetime import datetime, timedelta
# from log import DEBUG, ERROR
@ -27,6 +29,9 @@ class Window(QMainWindow, Ui_MainWindow):
super().__init__(parent)
self.setupUi(self)
self.timer = QTimer()
self.even_tick = True
self.music = music.Music()
self.playlist.music = self.music
self.connect_signals_slots()
self.disable_play_next_controls()
@ -41,9 +46,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.setGeometry(x, y, width, height)
# Hard code to the only playlist we have for now
self.playlist.load_playlist("Default")
self.timer.start(Config.TIMER_MS)
if self.playlist.load_playlist("Default"):
self.enable_play_next_controls()
self.timer.start(Config.TIMER_MS)
def __del__(self):
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.actionFade.triggered.connect(self.fade)
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.btnPrevious.clicked.connect(self.play_previous)
self.btnSearchDatabase.clicked.connect(self.search_database)
@ -94,37 +98,38 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick)
def disable_play_next_controls(self):
self.actionPlay_selected.setEnabled(False)
self.actionPlay_next.setEnabled(False)
def enable_play_next_controls(self):
self.actionPlay_selected.setEnabled(True)
self.actionPlay_next.setEnabled(True)
def fade(self):
self.playlist.fade()
def play_next(self):
self.music.play_next()
self.current_track.setText(
f"{self.music.get_current_title()} - "
f"{self.music.get_current_artist()}"
)
self.playlist.play_next()
self.disable_play_next_controls()
self.previous_track.setText(
f"{self.music.get_previous_title()} - "
f"{self.music.get_previous_artist()}"
f"{self.playlist.get_previous_title()} - "
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
now = datetime.now()
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)
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
self.label_fade_length.setText(helpers.ms_to_mmss(
silence_at - self.music.get_current_fade_at()))
self.set_playlist_colours()
silence_at - self.playlist.get_current_fade_at()))
def play_previous(self):
"Resume playing last track"
@ -136,58 +141,54 @@ class Window(QMainWindow, Ui_MainWindow):
dlg = DbDialog(self)
dlg.exec()
def set_next_track(self):
# TODO
pass
def tick(self):
pass
# self.current_time.setText(now.strftime("%H:%M:%S"))
# 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
"""
Update screen
The Time of Day clock is updated every tick (500ms).
def update_tod_clock(self):
"Update time of day clock"
All other timers are updated every second. As the timers have a
one-second resolution, updating every 500ms can result in some
timers updating and then, 500ms later, other timers updating. That
looks odd.
"""
# TODO
pass
now = datetime.now()
def update_track_headers(self):
"Update last/current/next track header"
self.lblTOD.setText(now.strftime("%H:%M:%S"))
# TODO
pass
self.even_tick = not self.even_tick
if not self.even_tick:
return
def update_track_clocks(self):
"Update started at, silent at, clock times"
if self.music.playing():
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):
def __init__(self, parent=None):

View File

@ -10,11 +10,10 @@ from PyQt5.QtWidgets import (
)
import helpers
import music
from config import Config
from log import DEBUG
from model import Playlists, Settings, Tracks
from log import DEBUG, ERROR
from model import Playdates, Playlists, Settings, Tracks
class Playlist(QTableWidget):
@ -27,11 +26,6 @@ class Playlist(QTableWidget):
COL_ENDTIME = 5
COL_PATH = 6
# UserRoles
NEXT_TRACK = 1
CURRENT_TRACK = 2
COMMENT = 3
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -45,8 +39,6 @@ class Playlist(QTableWidget):
self.setSelectionBehavior(QAbstractItemView.SelectRows)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.music = music.Music()
self.current_track = None
self.next_track = None
self.previous_track_path = None
@ -136,9 +128,13 @@ class Playlist(QTableWidget):
self.item(drop_row + row_index,
column_index).setSelected(True)
super().dropEvent(event)
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()
def get_current_artist(self):
@ -220,14 +216,100 @@ class Playlist(QTableWidget):
)
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():
self.add_to_playlist(track)
# Set the first playable track as next to play
for row in range(self.rowCount()):
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):
"""
@ -236,34 +318,68 @@ class Playlist(QTableWidget):
If there is no next track set, return.
If there's currently a track playing, fade it.
Move next track to current track.
Cue up next track in playlist if there is one.
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
Remember it was played this session
Remember it was played for this session
Update playlist appearance.
"""
# If there is no next track set, return.
if not self.next_track:
return
# If there's currently a track playing, fade it.
if self.music.playing():
path, position = self.music.fade()
self.previous_track_path = path
self.previous_track_position = position
# Move next track to current track.
self.current_track = self.next_track
# Play (new) current.
self.music.play(self.current_track.path)
# Find the playlist row for current track
current_track_id = self.current_track.track_id
self.current_track_row = None
for row in range(self.rowCount):
if self.item(row, 0):
if current_track_id == int(self.item(row, 0).text()):
self.current_track_row = row
break
# Update playlist "current track" metadata
old_current = self.meta_get_current()
if old_current is not None:
self.meta_clear(old_current)
current = self.meta_get_next()
if current is not None:
self.meta_set_current(current)
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):
"""
@ -302,30 +418,33 @@ class Playlist(QTableWidget):
DEBUG(f"set_row_as_next_track: track_id={track_id}")
self.next_track = Tracks.get_track(track_id)
# Mark row as next track
self.item(row, self.COL_INDEX).setData(
Qt.UserRole, self.NEXT_TRACK)
self.set_playlist_colours()
self.meta_set_next(row)
return True
return False
def set_playlist_colours(self):
def update_playlist_colours(self):
self.clearSelection()
current = self.meta_get_current()
next = self.meta_get_next()
notes = self.meta_get_notes()
for row in range(self.rowCount()):
if self.item(row, self.COL_INDEX):
data = self.item(row, self.COL_INDEX).data(Qt.UserRole)
if data == self.NEXT_TRACK:
self.set_row_colour(
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
elif data == self.CURRENT_TRACK:
self.set_row_colour(
row, QColor(Config.COLOUR_CURRENT_PLAYLIST))
if row == current:
self.set_row_colour(
row, QColor(Config.COLOUR_CURRENT_PLAYLIST))
elif row == next:
self.set_row_colour(
row, QColor(Config.COLOUR_NEXT_PLAYLIST))
elif row in notes:
self.set_row_colour(
row, QColor(Config.COLOUR_NOTES_PLAYLIST))
else:
if row % 2:
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
else:
if row % 2:
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
else:
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
self.set_row_colour(row, colour)
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
self.set_row_colour(row, colour)
def set_row_colour(self, row, colour):
for j in range(self.columnCount()):

View File

@ -723,8 +723,8 @@ border: 1px solid rgb(85, 87, 83);</string>
<property name="title">
<string>Pla&amp;ylist</string>
</property>
<addaction name="actionPlay_selected"/>
<addaction name="actionPlay_next"/>
<addaction name="actionSkip_next"/>
<addaction name="actionSearch_database"/>
<addaction name="actionAdd_file"/>
<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>
</property>
</widget>
<action name="actionPlay_selected">
<action name="actionPlay_next">
<property name="icon">
<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>
</property>
</action>
<action name="actionPlay_next">
<action name="actionSkip_next">
<property name="icon">
<iconset>
<normaloff>icon-play-next.png</normaloff>icon-play-next.png</iconset>