Playlist creation and changing fragilely working

This commit is contained in:
Keith Edmunds 2021-04-07 22:07:53 +01:00
parent 15a540234e
commit 67b3b804e1
5 changed files with 248 additions and 91 deletions

View File

@ -147,13 +147,26 @@ class Playlists(Base):
def __repr__(self): def __repr__(self):
return (f"<Playlist(id={self.id}, name={self.name}>") return (f"<Playlist(id={self.id}, name={self.name}>")
# Currently we only support one playlist, so make that obvious from @staticmethod
# function name def new(name):
@classmethod DEBUG(f"Playlists.new({name})")
def get_playlist_by_name(cls, name): pl = Playlists()
"Returns a playlist object for named playlist" pl.name = name
session.add(pl)
session.commit()
return pl.id
return session.query(Playlists).filter(Playlists.name == name).one() @staticmethod
def get_all_playlists():
"Returns a list of (id, name) of all playlists"
return session.query(Playlists).all()
@classmethod
def get_playlist_by_id(cls, plid):
"Returns a playlist object for playlist id"
return session.query(Playlists).filter(Playlists.id == plid).one()
def add_track(self, track, row): def add_track(self, track, row):
glue = PlaylistTracks(row=row) glue = PlaylistTracks(row=row)
@ -178,14 +191,18 @@ class PlaylistTracks(Base):
playlists = relationship("Playlists", back_populates="tracks") playlists = relationship("Playlists", back_populates="tracks")
@staticmethod @staticmethod
def update_track_row(playlist_id, track_id, row): def update_track_row(playlist_id, track_id, old_row, new_row):
DEBUG(f"update_track({id}, {row})") DEBUG(
f"update_track_row({playlist_id}, "
f"{track_id}, {old_row}, {new_row})"
)
plt = session.query(PlaylistTracks).filter( plt = session.query(PlaylistTracks).filter(
PlaylistTracks.playlist_id == playlist_id, PlaylistTracks.playlist_id == playlist_id,
PlaylistTracks.track_id == track_id PlaylistTracks.track_id == track_id,
PlaylistTracks.row == old_row
).one() ).one()
plt.row = row plt.row = new_row
session.commit() session.commit()
@staticmethod @staticmethod

View File

@ -7,21 +7,19 @@ import music
from datetime import datetime, timedelta from datetime import datetime, timedelta
# from log import DEBUG # from log import DEBUG
from PyQt5.QtCore import Qt, QTimer from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
QDialog,
QFileDialog, QFileDialog,
QInputDialog, QInputDialog,
QListWidgetItem, QLabel,
QMainWindow, QMainWindow,
) )
import helpers import helpers
from config import Config from config import Config
from model import Playlists, Settings, Tracks from model import Settings
from ui.dlg_search_database_ui import Ui_Dialog
from ui.main_window_ui import Ui_MainWindow from ui.main_window_ui import Ui_MainWindow
@ -47,10 +45,15 @@ class Window(QMainWindow, Ui_MainWindow):
height = record.f_int or 981 height = record.f_int or 981
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 first playlist for now
if self.playlist.load_playlist("Default"): # TODO
if self.playlist.load_playlist(1):
self.update_headers() self.update_headers()
self.enable_play_next_controls() self.enable_play_next_controls()
self.plLabel = QLabel(f"Playlist: {self.playlist.playlist_name}")
self.statusbar.addPermanentWidget(self.plLabel)
self.timer.start(Config.TIMER_MS) self.timer.start(Config.TIMER_MS)
def __del__(self): def __del__(self):
@ -90,6 +93,8 @@ class Window(QMainWindow, Ui_MainWindow):
def connect_signals_slots(self): def connect_signals_slots(self):
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.actionNewPlaylist.triggered.connect(self.create_playlist)
self.actionSelectPlaylist.triggered.connect(self.select_playlist)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.search_database) self.actionSearch_database.triggered.connect(self.search_database)
self.actionSkip_next.triggered.connect(self.play_next) self.actionSkip_next.triggered.connect(self.play_next)
@ -102,6 +107,17 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
def create_playlist(self):
"Create new playlist"
dlg = QInputDialog(self)
dlg.setInputMode(QInputDialog.TextInput)
dlg.setLabelText("Playlist name:")
dlg.resize(500, 100)
ok = dlg.exec()
if ok:
self.playlist.create_playlist(dlg.textValue())
def disable_play_next_controls(self): def disable_play_next_controls(self):
self.actionPlay_next.setEnabled(False) self.actionPlay_next.setEnabled(False)
@ -143,8 +159,10 @@ class Window(QMainWindow, Ui_MainWindow):
pass pass
def search_database(self): def search_database(self):
dlg = DbDialog(self) self.playlist.search_database()
dlg.exec()
def select_playlist(self):
self.playlist.select_playlist()
def set_next_track(self): def set_next_track(self):
"Set selected track as next" "Set selected track as next"
@ -237,62 +255,8 @@ class Window(QMainWindow, Ui_MainWindow):
f"{self.playlist.get_next_artist()}" f"{self.playlist.get_next_artist()}"
) )
def update_statusbar(self):
class AddNoteDialog(QDialog): pass
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_dlgAddNote()
self.ui.setupUi(self)
class DbDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.ui.matchList.itemDoubleClicked.connect(self.listdclick)
record = Settings.get_int("dbdialog_width")
width = record.f_int or 800
record = Settings.get_int("dbdialog_height")
height = record.f_int or 600
self.resize(width, height)
def __del__(self):
record = Settings.get_int("dbdialog_height")
if record.f_int != self.height():
record.update({'f_int': self.height()})
record = Settings.get_int("dbdialog_width")
if record.f_int != self.width():
record.update({'f_int': self.width()})
def chars_typed(self, s):
if len(s) >= 3:
matches = Tracks.search_titles(s)
self.ui.matchList.clear()
if matches:
for track in matches:
t = QListWidgetItem()
t.setText(
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]"
)
t.setData(Qt.UserRole, track.id)
self.ui.matchList.addItem(t)
def listdclick(self, entry):
track_id = entry.data(Qt.UserRole)
track = Tracks.track_from_id(track_id)
# Store in current playlist in database
db_playlist = Playlists.get_only_playlist()
# TODO: hack position to be at end of list
db_playlist.add_track(track, 99999)
# Add to on-screen playlist
self.parent().add_to_playlist(track)
def main(): def main():

View File

@ -5,7 +5,9 @@ from PyQt5.QtGui import QColor, QDropEvent
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication, QApplication,
QDialog,
QHBoxLayout, QHBoxLayout,
QListWidgetItem,
QMenu, QMenu,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
@ -19,6 +21,8 @@ from config import Config
from datetime import datetime, timedelta from datetime import datetime, timedelta
from log import DEBUG, ERROR from log import DEBUG, ERROR
from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks from model import Notes, Playdates, Playlists, PlaylistTracks, Settings, Tracks
from ui.dlg_search_database_ui import Ui_Dialog
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
class Playlist(QTableWidget): class Playlist(QTableWidget):
@ -199,6 +203,12 @@ class Playlist(QTableWidget):
if repaint: if repaint:
self.repaint() self.repaint()
def create_playlist(self, name):
"Create new playlist"
new_id = Playlists.new(name)
self.load_playlist(new_id)
def drop_on(self, event): def drop_on(self, event):
index = self.indexAt(event.pos()) index = self.indexAt(event.pos())
if not index.isValid(): if not index.isValid():
@ -327,21 +337,21 @@ class Playlist(QTableWidget):
and pos.y() >= rect.center().y() # noqa W503 and pos.y() >= rect.center().y() # noqa W503
) )
def load_playlist(self, name): def load_playlist(self, plid):
""" """
Load tracks and notes from named playlist. Load tracks and notes from playlist id.
Set first track as next track to play. Set first track as next track to play.
Return True if successful else False. Return True if successful else False.
""" """
DEBUG(f"load_playlist({name})") DEBUG(f"load_playlist({plid})")
self.playlist_name = name p = Playlists.get_playlist_by_id(plid)
self.playlist_id = plid
p = Playlists.get_playlist_by_name(name) self.playlist_name = p.name
self.playlist_id = p.id self.parent().parent().update_statusbar()
# We need to retrieve playlist tracks and playlist notes, then # We need to retrieve playlist tracks and playlist notes, then
# add them in row order. We don't mandate that an item will be # add them in row order. We don't mandate that an item will be
@ -354,6 +364,9 @@ class Playlist(QTableWidget):
for n in p.notes: for n in p.notes:
data.append(([n.row], n)) data.append(([n.row], n))
# Clear playlist
self.setRowCount(0)
# Now add data in row order # Now add data in row order
for item in sorted(data, key=lambda x: x[0]): for item in sorted(data, key=lambda x: x[0]):
DEBUG(f"Adding {item}") DEBUG(f"Adding {item}")
@ -516,6 +529,14 @@ class Playlist(QTableWidget):
# Update display # Update display
self.tracks_changed() self.tracks_changed()
def search_database(self):
dlg = DbDialog(self)
dlg.exec()
def select_playlist(self):
dlg = SelectPlaylistDialog(self)
dlg.exec()
def set_selected_as_next(self): def set_selected_as_next(self):
""" """
Sets the selected track as the next track. Sets the selected track as the next track.
@ -647,7 +668,7 @@ class Playlist(QTableWidget):
# Get tracks and notes from database # Get tracks and notes from database
db_tracks = {} db_tracks = {}
db_notes = {} db_notes = {}
p = Playlists.get_playlist_by_name(self.playlist_name) p = Playlists.get_playlist_by_id(self.playlist_id)
for track in p.tracks: for track in p.tracks:
db_tracks[track.track_id] = track.row db_tracks[track.track_id] = track.row
@ -682,7 +703,8 @@ class Playlist(QTableWidget):
if tracks[id] != db_tracks[id]: if tracks[id] != db_tracks[id]:
DEBUG(f"Update db track.id={id} in database") DEBUG(f"Update db track.id={id} in database")
PlaylistTracks.update_track_row( PlaylistTracks.update_track_row(
self.playlist_id, id, row=tracks[id] self.playlist_id, track_id=id,
old_row=db_tracks[id], new_row=tracks[id]
) )
def set_row_bold(self, row): def set_row_bold(self, row):
@ -749,6 +771,76 @@ class Playlist(QTableWidget):
self.repaint() self.repaint()
class DbDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_Dialog()
self.ui.setupUi(self)
self.ui.searchString.textEdited.connect(self.chars_typed)
self.ui.matchList.itemDoubleClicked.connect(self.listdclick)
record = Settings.get_int("dbdialog_width")
width = record.f_int or 800
record = Settings.get_int("dbdialog_height")
height = record.f_int or 600
self.resize(width, height)
def __del__(self):
record = Settings.get_int("dbdialog_height")
if record.f_int != self.height():
record.update({'f_int': self.height()})
record = Settings.get_int("dbdialog_width")
if record.f_int != self.width():
record.update({'f_int': self.width()})
def chars_typed(self, s):
if len(s) >= 3:
matches = Tracks.search_titles(s)
self.ui.matchList.clear()
if matches:
for track in matches:
t = QListWidgetItem()
t.setText(
f"{track.title} - {track.artist} "
f"[{helpers.ms_to_mmss(track.duration)}]"
)
t.setData(Qt.UserRole, track.id)
self.ui.matchList.addItem(t)
def listdclick(self, entry):
track_id = entry.data(Qt.UserRole)
track = Tracks.track_from_id(track_id)
# Store in current playlist in database
db_playlist = Playlists.get_playlist_by_id(self.parent().playlist_id)
# TODO: hack position to be at end of list
db_playlist.add_track(track, 99999)
# Add to on-screen playlist
self.parent().add_to_playlist(track)
class SelectPlaylistDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_dlgSelectPlaylist()
self.ui.setupUi(self)
self.ui.lstPlaylists.itemDoubleClicked.connect(self.listdclick)
for (plid, plname) in [
(a.id, a.name) for a in Playlists.get_all_playlists()
]:
p = QListWidgetItem()
p.setText(plname)
p.setData(Qt.UserRole, plid)
self.ui.lstPlaylists.addItem(p)
def listdclick(self, entry):
plid = entry.data(Qt.UserRole)
self.parent().load_playlist(plid)
class Window(QWidget): class Window(QWidget):
def __init__(self): def __init__(self):
super(Window, self).__init__() super(Window, self).__init__()

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>dlgSelectPlaylist</class>
<widget class="QDialog" name="dlgSelectPlaylist">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>276</width>
<height>150</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="lstPlaylists"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>dlgSelectPlaylist</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>dlgSelectPlaylist</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>768</width> <width>931</width>
<height>600</height> <height>600</height>
</rect> </rect>
</property> </property>
@ -710,7 +710,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>768</width> <width>931</width>
<height>18</height> <height>18</height>
</rect> </rect>
</property> </property>
@ -724,19 +724,26 @@ 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_next"/>
<addaction name="actionSkip_next"/>
<addaction name="actionSearch_database"/> <addaction name="actionSearch_database"/>
<addaction name="actionAdd_file"/> <addaction name="actionAdd_file"/>
<addaction name="action_Clear_selection"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSelectPlaylist"/>
<addaction name="actionNewPlaylist"/>
</widget>
<widget class="QMenu" name="menu_Tracks">
<property name="title">
<string>&amp;Tracks</string>
</property>
<addaction name="actionPlay_next"/>
<addaction name="actionSkip_next"/>
<addaction name="actionFade"/> <addaction name="actionFade"/>
<addaction name="actionS_top"/> <addaction name="actionS_top"/>
<addaction name="separator"/>
<addaction name="action_Resume_previous"/> <addaction name="action_Resume_previous"/>
<addaction name="action_Clear_selection"/>
</widget> </widget>
<addaction name="menuFile"/> <addaction name="menuFile"/>
<addaction name="menuPlaylist"/> <addaction name="menuPlaylist"/>
<addaction name="menu_Tracks"/>
</widget> </widget>
<widget class="QStatusBar" name="statusbar"> <widget class="QStatusBar" name="statusbar">
<property name="enabled"> <property name="enabled">
@ -833,6 +840,16 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>&amp;Test</string> <string>&amp;Test</string>
</property> </property>
</action> </action>
<action name="actionSelectPlaylist">
<property name="text">
<string>Select &amp;playlist</string>
</property>
</action>
<action name="actionNewPlaylist">
<property name="text">
<string>&amp;New playlist</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>