Refactoring mostly done; manage playlist metadata
This commit is contained in:
parent
bcfd076a93
commit
dadd251587
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
201
app/playlists.py
201
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()):
|
||||
|
||||
@ -723,8 +723,8 @@ border: 1px solid rgb(85, 87, 83);</string>
|
||||
<property name="title">
|
||||
<string>Pla&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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user