diff --git a/app/config.py b/app/config.py
index 094cd25..655bdbe 100644
--- a/app/config.py
+++ b/app/config.py
@@ -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
diff --git a/app/musicmuster.py b/app/musicmuster.py
index 257c64a..f7b4476 100755
--- a/app/musicmuster.py
+++ b/app/musicmuster.py
@@ -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):
diff --git a/app/playlists.py b/app/playlists.py
index 1920bfe..314ff65 100644
--- a/app/playlists.py
+++ b/app/playlists.py
@@ -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()):
diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui
index 3c5f4b9..646aeff 100644
--- a/app/ui/main_window.ui
+++ b/app/ui/main_window.ui
@@ -723,8 +723,8 @@ border: 1px solid rgb(85, 87, 83);
Pla&ylist
-
+
@@ -745,7 +745,7 @@ border: 1px solid rgb(85, 87, 83);
background-color: rgb(211, 215, 207);
-
+
icon-play.pngicon-play.png
@@ -757,7 +757,7 @@ border: 1px solid rgb(85, 87, 83);
Return
-
+
icon-play-next.pngicon-play-next.png