874 lines
28 KiB
Python
874 lines
28 KiB
Python
from PyQt5 import QtCore
|
|
from PyQt5.QtCore import Qt
|
|
from PyQt5.Qt import QFont
|
|
from PyQt5.QtGui import QColor, QDropEvent
|
|
from PyQt5.QtWidgets import (
|
|
QAbstractItemView,
|
|
QApplication,
|
|
QDialog,
|
|
QHBoxLayout,
|
|
QListWidgetItem,
|
|
QMenu,
|
|
QMessageBox,
|
|
QTableWidget,
|
|
QTableWidgetItem,
|
|
QWidget,
|
|
)
|
|
|
|
import helpers
|
|
|
|
from config import Config
|
|
from datetime import datetime, timedelta
|
|
from log import DEBUG, ERROR
|
|
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):
|
|
# Column names
|
|
COL_INDEX = 0
|
|
COL_MSS = 1
|
|
COL_NOTE = 1
|
|
COL_TITLE = 2
|
|
COL_ARTIST = 3
|
|
COL_DURATION = 4
|
|
COL_ENDTIME = 5
|
|
COL_PATH = 6
|
|
|
|
NOTE_COL_SPAN = 6
|
|
NOTE_ROW_SPAN = 1
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
|
|
self.setDragEnabled(True)
|
|
self.setAcceptDrops(True)
|
|
self.viewport().setAcceptDrops(True)
|
|
self.setDragDropOverwriteMode(False)
|
|
self.setDropIndicatorShown(True)
|
|
|
|
self.setSelectionMode(QAbstractItemView.ExtendedSelection)
|
|
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
self.setDragDropMode(QAbstractItemView.InternalMove)
|
|
|
|
# This property holds how the widget shows a context menu
|
|
self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
# This signal is emitted when the widget's contextMenuPolicy is
|
|
# Qt::CustomContextMenu, and the user has requested a context
|
|
# menu on the widget.
|
|
self.customContextMenuRequested.connect(self.context_menu)
|
|
self.viewport().installEventFilter(self)
|
|
|
|
self.current_track = None
|
|
self.next_track = None
|
|
self.playlist_name = None
|
|
self.playlist_id = 0
|
|
self.previous_track = None
|
|
self.previous_track_position = None
|
|
self.played_tracks = []
|
|
|
|
# Column widths from settings
|
|
for column in range(self.columnCount()):
|
|
name = f"playlist_col_{str(column)}_width"
|
|
record = Settings.get_int(name)
|
|
if record.f_int is not None:
|
|
self.setColumnWidth(column, record.f_int)
|
|
|
|
def __del__(self):
|
|
"Save column widths"
|
|
|
|
for column in range(self.columnCount()):
|
|
width = self.columnWidth(column)
|
|
name = f"playlist_col_{str(column)}_width"
|
|
record = Settings.get_int(name)
|
|
if record.f_int != self.columnWidth(column):
|
|
record.update({'f_int': width})
|
|
|
|
def eventFilter(self, source, event):
|
|
if(event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504
|
|
event.buttons() == QtCore.Qt.RightButton and # noqa W504
|
|
source is self.viewport()):
|
|
item = self.itemAt(event.pos())
|
|
if item is not None:
|
|
row = item.row()
|
|
DEBUG(f"Right-click on row {row}")
|
|
self.menu = QMenu(self)
|
|
if row not in self.meta_get_notes():
|
|
act_setnext = self.menu.addAction("Set next")
|
|
act_setnext.triggered.connect(lambda: self.set_next(row))
|
|
self.menu.addSeparator()
|
|
act_delete = self.menu.addAction('Delete')
|
|
act_delete.triggered.connect(lambda: self.delete_row(row))
|
|
|
|
return super(Playlist, self).eventFilter(source, event)
|
|
|
|
def context_menu(self, pos):
|
|
|
|
self.menu.exec_(self.mapToGlobal(pos))
|
|
|
|
def delete_row(self, row):
|
|
"Delete row"
|
|
|
|
notes_rows = self.meta_get_notes()
|
|
if row in notes_rows:
|
|
# TODO
|
|
DEBUG("Delete notes not yet implemented")
|
|
elif row == self.meta_get_current():
|
|
# TODO
|
|
DEBUG("Can't delete playing track")
|
|
elif row == self.meta_get_next():
|
|
# TODO
|
|
DEBUG("Can't delete next track")
|
|
|
|
else:
|
|
track_title = self.item(row, self.COL_TITLE).text()
|
|
|
|
msg = QMessageBox()
|
|
msg.setIcon(QMessageBox.Warning)
|
|
msg.setText(f"Delete '{track_title}'?")
|
|
msg.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
|
|
msg.setDefaultButton(QMessageBox.Cancel)
|
|
msg.setWindowTitle("Delete row")
|
|
if msg.exec() == QMessageBox.Yes:
|
|
DEBUG(f"Delete row {row}")
|
|
track_id = int(self.item(row, self.COL_INDEX).text())
|
|
PlaylistTracks.remove_track(self.playlist_id, track_id)
|
|
self.removeRow(row)
|
|
|
|
self.repaint()
|
|
|
|
def add_note(self, text):
|
|
"""
|
|
Add note to playlist
|
|
|
|
If a row is selected, add note above. Otherwise, add to end of
|
|
playlist.
|
|
"""
|
|
|
|
DEBUG(f"add_note({text})")
|
|
|
|
DEBUG(f"self.currentRow()={self.currentRow()}")
|
|
row = self.currentRow()
|
|
if row < 0:
|
|
row = self.rowCount()
|
|
DEBUG(f"row={row}")
|
|
note_id = Notes.add_note(self.playlist_id, row, text)
|
|
|
|
self.insertRow(row)
|
|
item = QTableWidgetItem(str(note_id))
|
|
self.setItem(row, self.COL_INDEX, item)
|
|
item = QTableWidgetItem(text)
|
|
self.setItem(row, self.COL_NOTE, item)
|
|
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
|
|
self.NOTE_COL_SPAN)
|
|
self.meta_set_note(row)
|
|
|
|
self.repaint()
|
|
|
|
def add_to_playlist(self, data, repaint=True):
|
|
"""
|
|
Add data to playlist. Data may be either a Tracks object or a
|
|
Notes object.
|
|
"""
|
|
|
|
row = self.rowCount()
|
|
self.insertRow(row)
|
|
DEBUG(f"Adding to row {row}")
|
|
if isinstance(data, Tracks):
|
|
track = data
|
|
item = QTableWidgetItem(str(track.id))
|
|
self.setItem(row, self.COL_INDEX, item)
|
|
item = QTableWidgetItem(str(track.start_gap))
|
|
self.setItem(row, self.COL_MSS, item)
|
|
item = QTableWidgetItem(track.title)
|
|
self.setItem(row, self.COL_TITLE, item)
|
|
item = QTableWidgetItem(track.artist)
|
|
self.setItem(row, self.COL_ARTIST, item)
|
|
item = QTableWidgetItem(helpers.ms_to_mmss(track.duration))
|
|
self.setItem(row, self.COL_DURATION, item)
|
|
item = QTableWidgetItem(track.path)
|
|
self.setItem(row, self.COL_PATH, item)
|
|
DEBUG(f"add_to_playlist: track.id={track.id}")
|
|
else:
|
|
# This is a note
|
|
item = QTableWidgetItem(str(data.id))
|
|
self.setItem(row, self.COL_INDEX, item)
|
|
item = QTableWidgetItem(data.note)
|
|
self.setItem(row, self.COL_NOTE, item)
|
|
self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN,
|
|
self.NOTE_COL_SPAN)
|
|
self.meta_set_note(row)
|
|
|
|
if 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):
|
|
index = self.indexAt(event.pos())
|
|
if not index.isValid():
|
|
return self.rowCount()
|
|
|
|
return (index.row() + 1 if self.is_below(event.pos(), index)
|
|
else index.row())
|
|
|
|
def dropEvent(self, event: QDropEvent):
|
|
if not event.isAccepted() and event.source() == self:
|
|
drop_row = self.drop_on(event)
|
|
|
|
rows = sorted(set(item.row() for item in self.selectedItems()))
|
|
rows_to_move = [
|
|
[QTableWidgetItem(self.item(row_index, column_index)) for
|
|
column_index in range(self.columnCount())]
|
|
for row_index in rows
|
|
]
|
|
for row_index in reversed(rows):
|
|
self.removeRow(row_index)
|
|
if row_index < drop_row:
|
|
drop_row -= 1
|
|
|
|
for row_index, data in enumerate(rows_to_move):
|
|
row_index += drop_row
|
|
self.insertRow(row_index)
|
|
for column_index, column_data in enumerate(data):
|
|
self.setItem(row_index, column_index, column_data)
|
|
event.accept()
|
|
for row_index in range(len(rows_to_move)):
|
|
for column_index in range(self.columnCount()):
|
|
self.item(drop_row + row_index,
|
|
column_index).setSelected(True)
|
|
super().dropEvent(event)
|
|
|
|
DEBUG(f"Moved row(s) {rows} to become row {drop_row}")
|
|
|
|
self.clearSelection()
|
|
self.repaint()
|
|
|
|
def fade(self):
|
|
self.music.fade()
|
|
|
|
def get_current_artist(self):
|
|
try:
|
|
return self.current_track.artist
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_current_duration(self):
|
|
try:
|
|
return self.current_track.duration
|
|
except AttributeError:
|
|
return 0
|
|
|
|
def get_current_fade_at(self):
|
|
try:
|
|
return self.current_track.fade_at
|
|
except AttributeError:
|
|
return 0
|
|
|
|
def get_current_playtime(self):
|
|
|
|
return self.music.get_playtime()
|
|
|
|
def get_current_silence_at(self):
|
|
try:
|
|
return self.current_track.silence_at
|
|
except AttributeError:
|
|
return 0
|
|
|
|
def get_current_title(self):
|
|
try:
|
|
return self.current_track.title
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_next_artist(self):
|
|
try:
|
|
return self.next_track.artist
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_next_title(self):
|
|
try:
|
|
return self.next_track.title
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_next_track_id(self):
|
|
try:
|
|
return self.next_track.id
|
|
except AttributeError:
|
|
return 0
|
|
|
|
def get_previous_artist(self):
|
|
try:
|
|
return self.previous_track.artist
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_previous_title(self):
|
|
try:
|
|
return self.previous_track.title
|
|
except AttributeError:
|
|
return ""
|
|
|
|
def get_row_endtime(self, row, start):
|
|
"Return this row's end time given its start time"
|
|
|
|
duration = Tracks.get_duration(
|
|
int(self.item(row, self.COL_INDEX).text()))
|
|
return start + timedelta(milliseconds=duration)
|
|
|
|
def is_below(self, pos, index):
|
|
rect = self.visualRect(index)
|
|
margin = 2
|
|
if pos.y() - rect.top() < margin:
|
|
return False
|
|
elif rect.bottom() - pos.y() < margin:
|
|
return True
|
|
# noinspection PyTypeChecker
|
|
return (
|
|
rect.contains(pos, True) and not
|
|
(int(self.model().flags(index)) & Qt.ItemIsDropEnabled)
|
|
and pos.y() >= rect.center().y() # noqa W503
|
|
)
|
|
|
|
def load_playlist(self, plid):
|
|
"""
|
|
Load tracks and notes from playlist id.
|
|
|
|
Set first track as next track to play.
|
|
|
|
Return True if successful else False.
|
|
"""
|
|
|
|
DEBUG(f"load_playlist({plid})")
|
|
|
|
p = Playlists.get_playlist_by_id(plid)
|
|
self.playlist_id = plid
|
|
self.playlist_name = p.name
|
|
self.parent().parent().update_statusbar()
|
|
|
|
# We need to retrieve playlist tracks and playlist notes, then
|
|
# add them in row order. We don't mandate that an item will be
|
|
# on its specified row, only that it will be above
|
|
# larger-numbered row items, and below lower-numbered ones.
|
|
data = []
|
|
|
|
for t in p.tracks:
|
|
data.append(([t.row], t.tracks))
|
|
for n in p.notes:
|
|
data.append(([n.row], n))
|
|
|
|
# Clear playlist
|
|
self.setRowCount(0)
|
|
|
|
# Now add data in row order
|
|
for item in sorted(data, key=lambda x: x[0]):
|
|
DEBUG(f"Adding {item}")
|
|
self.add_to_playlist(item[1], repaint=False)
|
|
|
|
# Set the first non-notes row as next track to play
|
|
notes_rows = self.meta_get_notes()
|
|
for row in range(self.rowCount()):
|
|
if row in notes_rows:
|
|
continue
|
|
self.meta_set_next(row)
|
|
self.tracks_changed()
|
|
return True
|
|
|
|
return False
|
|
|
|
def meta_clear(self, row):
|
|
"Clear metadata 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"
|
|
|
|
DEBUG(f"meta_set_current({row})")
|
|
|
|
old_current = self.meta_get_current()
|
|
if old_current is not None:
|
|
self.meta_clear(old_current)
|
|
self.meta_set(row, "current")
|
|
|
|
def meta_set_next(self, row):
|
|
"Mark row as next track"
|
|
|
|
DEBUG(f"meta_set_next({row})")
|
|
|
|
old_next = self.meta_get_next()
|
|
if old_next is not None:
|
|
self.meta_clear(old_next)
|
|
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):
|
|
"""
|
|
Play next track.
|
|
|
|
If there is no next track set, return.
|
|
If there's currently a track playing, fade it.
|
|
Move next track to current track.
|
|
Play (new) current.
|
|
Update playlist "current track" metadata
|
|
Cue up next track in playlist if there is one.
|
|
Tell database to record it as played
|
|
Remember it was played for this session
|
|
Update metadata and headers, and repaint
|
|
"""
|
|
|
|
# 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():
|
|
self.previous_track_position = self.music.fade()
|
|
self.previous_track = self.current_track
|
|
|
|
# Shuffle tracks along
|
|
self.current_track = self.next_track
|
|
self.next_track = None
|
|
|
|
# Play (new) current.
|
|
self.music.play(self.current_track.path)
|
|
|
|
# Update metadata
|
|
self.meta_set_current(self.meta_get_next())
|
|
|
|
# Set track start time
|
|
current_row = self.meta_get_current()
|
|
endtime = datetime.now() + timedelta(
|
|
milliseconds=self.current_track.silence_at)
|
|
item = QTableWidgetItem(endtime.strftime("%H:%M:%S"))
|
|
self.setItem(current_row, self.COL_ENDTIME, item)
|
|
|
|
# Set up metadata for next track in playlist if there is one.
|
|
if current_row is not None:
|
|
start = current_row + 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:
|
|
self.meta_set_next(row)
|
|
break
|
|
|
|
# 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 display
|
|
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):
|
|
"""
|
|
Sets the selected track as the next track.
|
|
"""
|
|
|
|
self.set_next(self.currentRow())
|
|
|
|
def set_next(self, row):
|
|
"""
|
|
If passed row is track row, set that track as the next track to
|
|
be played and return True. Otherwise return False.
|
|
"""
|
|
|
|
if row in self.meta_get_notes():
|
|
return False
|
|
|
|
if self.item(row, self.COL_INDEX):
|
|
self.meta_set_next(row)
|
|
self.tracks_changed()
|
|
return True
|
|
|
|
return False
|
|
|
|
def music_ended(self):
|
|
"Update display"
|
|
|
|
self.previous_track = self.current_track
|
|
self.previous_track_position = 0
|
|
self.meta_clear(self.meta_get_current())
|
|
self.tracks_changed()
|
|
|
|
def repaint(self):
|
|
"Set row colours, fonts, etc, and save playlist"
|
|
|
|
self.save()
|
|
|
|
self.clearSelection()
|
|
current = self.meta_get_current()
|
|
next = self.meta_get_next()
|
|
notes = self.meta_get_notes()
|
|
|
|
# Set colours and start times
|
|
running_end_time = None
|
|
for row in range(self.rowCount()):
|
|
|
|
if row == current:
|
|
self.set_row_colour(
|
|
row, QColor(Config.COLOUR_CURRENT_PLAYLIST)
|
|
)
|
|
try:
|
|
running_end_time = datetime.strptime(self.item(
|
|
row, self.COL_ENDTIME).text(), "%H:%M:%S")
|
|
except AttributeError:
|
|
pass
|
|
self.set_row_bold(row)
|
|
|
|
elif row == next:
|
|
self.set_row_colour(
|
|
row, QColor(Config.COLOUR_NEXT_PLAYLIST)
|
|
)
|
|
if running_end_time:
|
|
running_end_time = self.get_row_endtime(
|
|
row, running_end_time)
|
|
item = QTableWidgetItem(
|
|
running_end_time.strftime("%H:%M:%S"))
|
|
self.setItem(row, self.COL_ENDTIME, item)
|
|
self.set_row_bold(row)
|
|
|
|
elif row in notes:
|
|
self.set_row_colour(
|
|
row, QColor(Config.COLOUR_NOTES_PLAYLIST)
|
|
)
|
|
self.set_row_bold(row)
|
|
|
|
else:
|
|
# Stripe rows
|
|
if row % 2:
|
|
colour = QColor(Config.COLOUR_ODD_PLAYLIST)
|
|
else:
|
|
colour = QColor(Config.COLOUR_EVEN_PLAYLIST)
|
|
self.set_row_colour(row, colour)
|
|
|
|
# Add running end time
|
|
if self.item(row, self.COL_INDEX):
|
|
if int(self.item(row, self.COL_INDEX).text()) > 0:
|
|
if running_end_time:
|
|
running_end_time = self.get_row_endtime(
|
|
row, running_end_time)
|
|
item = QTableWidgetItem(
|
|
running_end_time.strftime("%H:%M:%S"))
|
|
self.setItem(row, self.COL_ENDTIME, item)
|
|
|
|
# Dim played tracks
|
|
track_id = int(self.item(row, self.COL_INDEX).text())
|
|
if track_id in self.played_tracks:
|
|
self.set_row_not_bold(row)
|
|
else:
|
|
self.set_row_bold(row)
|
|
|
|
# Headers might need updating
|
|
self.parent().parent().update_headers()
|
|
|
|
def save(self):
|
|
"""
|
|
Save playlist to database.
|
|
|
|
Notes are also saved.
|
|
"""
|
|
|
|
# Create list of current tracks (row, track_id) for this playlst and
|
|
# compare with actual playlist. Fix as required.
|
|
# Repeat for notes (row, id, text)
|
|
|
|
tracks = {}
|
|
notes = {}
|
|
note_rows = self.meta_get_notes()
|
|
|
|
for row in range(self.rowCount()):
|
|
if self.item(row, self.COL_INDEX):
|
|
id = int(self.item(row, self.COL_INDEX).text())
|
|
else:
|
|
DEBUG(f"(no COL_INDEX data in row {row}")
|
|
continue
|
|
if row in note_rows:
|
|
notes[id] = (row, self.item(row, self.COL_NOTE).text())
|
|
else:
|
|
tracks[id] = row
|
|
|
|
# Get tracks and notes from database
|
|
db_tracks = {}
|
|
db_notes = {}
|
|
p = Playlists.get_playlist_by_id(self.playlist_id)
|
|
|
|
for track in p.tracks:
|
|
db_tracks[track.track_id] = track.row
|
|
|
|
for note in p.notes:
|
|
db_notes[note.id] = (note.row, note.note)
|
|
|
|
# Note ids to remove from db
|
|
for id in set(db_notes.keys()) - set(notes.keys()):
|
|
DEBUG(f"Delete note.id={id} from database")
|
|
|
|
# Note ids to add to db
|
|
for id in set(notes.keys()) - set(db_notes.keys()):
|
|
DEBUG(f"Add note.id={id} to database")
|
|
|
|
# Notes to update in db
|
|
for id in set(notes.keys()) & set(db_notes.keys()):
|
|
if notes[id] != db_notes[id]:
|
|
DEBUG(f"Update db note.id={id} in database")
|
|
Notes.update_note(id, row=notes[id][0], text=notes[id][1])
|
|
|
|
# Track ids to remove from db
|
|
for id in set(db_tracks.keys()) - set(tracks.keys()):
|
|
DEBUG(f"Delete track.id={id} from database")
|
|
|
|
# Track ids to add to db
|
|
for id in set(tracks.keys()) - set(db_tracks.keys()):
|
|
DEBUG(f"Add track.id={id} to database")
|
|
|
|
# Tracks to update in db
|
|
for id in set(tracks.keys()) & set(db_tracks.keys()):
|
|
if tracks[id] != db_tracks[id]:
|
|
DEBUG(f"Update db track.id={id} in database")
|
|
PlaylistTracks.update_track_row(
|
|
self.playlist_id, track_id=id,
|
|
old_row=db_tracks[id], new_row=tracks[id]
|
|
)
|
|
|
|
def set_row_bold(self, row):
|
|
bold = QFont()
|
|
bold.setBold(True)
|
|
for j in range(self.columnCount()):
|
|
if self.item(row, j):
|
|
self.item(row, j).setFont(bold)
|
|
|
|
def set_row_colour(self, row, colour):
|
|
for j in range(self.columnCount()):
|
|
if self.item(row, j):
|
|
self.item(row, j).setBackground(colour)
|
|
|
|
def set_row_not_bold(self, row):
|
|
normal = QFont()
|
|
normal.setBold(False)
|
|
for j in range(self.columnCount()):
|
|
if self.item(row, j):
|
|
self.item(row, j).setFont(normal)
|
|
|
|
def tracks_changed(self):
|
|
"""
|
|
One or more of current or next track has changed.
|
|
|
|
The row metadata is definitive because the same track may appear
|
|
more than once in the playlist, but only one track may be marked
|
|
as current or next.
|
|
|
|
Update self.current_track and self.next_track.
|
|
"""
|
|
|
|
# Update instance variables
|
|
current_row = self.meta_get_current()
|
|
if current_row is not None:
|
|
track_id = int(self.item(current_row, self.COL_INDEX).text())
|
|
if not self.current_track or self.current_track.id != track_id:
|
|
self.current_track = Tracks.get_track(track_id)
|
|
else:
|
|
self.current_track = None
|
|
|
|
next_row = self.meta_get_next()
|
|
if next_row is not None:
|
|
track_id = int(self.item(next_row, self.COL_INDEX).text())
|
|
if not self.next_track or self.next_track.id != track_id:
|
|
self.next_track = Tracks.get_track(track_id)
|
|
else:
|
|
self.next_track = None
|
|
|
|
try:
|
|
DEBUG(f"tracks_changed() previous={self.previous_track.id}")
|
|
except AttributeError:
|
|
DEBUG("tracks_changed() previous=None")
|
|
try:
|
|
DEBUG(f"tracks_changed() current={self.current_track.id}")
|
|
except AttributeError:
|
|
DEBUG("tracks_changed() current=None")
|
|
try:
|
|
DEBUG(f"tracks_changed() next={self.next_track.id}")
|
|
except AttributeError:
|
|
DEBUG("tracks_changed() next=None")
|
|
|
|
# Update display
|
|
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):
|
|
def __init__(self):
|
|
super(Window, self).__init__()
|
|
|
|
layout = QHBoxLayout()
|
|
self.setLayout(layout)
|
|
|
|
self.table_widget = Playlist()
|
|
layout.addWidget(self.table_widget)
|
|
|
|
# setup table widget
|
|
self.table_widget.setColumnCount(2)
|
|
self.table_widget.setHorizontalHeaderLabels(['Type', 'Name'])
|
|
|
|
items = [('Red', 'Toyota'), ('Blue', 'RV'), ('Green', 'Beetle'),
|
|
('Silver', 'Chevy'), ('Black', 'BMW')]
|
|
self.table_widget.setRowCount(len(items))
|
|
for i, (color, model) in enumerate(items):
|
|
self.table_widget.setItem(i, 0, QTableWidgetItem(color))
|
|
self.table_widget.setItem(i, 1, QTableWidgetItem(model))
|
|
|
|
self.resize(400, 400)
|
|
self.show()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
app = QApplication(sys.argv)
|
|
window = Window()
|
|
sys.exit(app.exec_())
|