Tabbed playlists working

This commit is contained in:
Keith Edmunds 2021-04-29 22:20:24 +01:00
parent ed2a766c80
commit 51b2dd43e5
5 changed files with 386 additions and 445 deletions

View File

@ -5,6 +5,7 @@ import sqlalchemy
from datetime import datetime from datetime import datetime
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import ( from sqlalchemy import (
Boolean,
Column, Column,
DateTime, DateTime,
Float, Float,
@ -153,6 +154,8 @@ class Playlists(Base):
id = Column(Integer, primary_key=True, autoincrement=True) id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String(32), nullable=False, unique=True) name = Column(String(32), nullable=False, unique=True)
last_used = Column(DateTime, default=None, nullable=True)
loaded = Column(Boolean, default=True)
notes = relationship("Notes", notes = relationship("Notes",
order_by="Notes.row", order_by="Notes.row",
back_populates="playlist") back_populates="playlist")
@ -172,6 +175,18 @@ class Playlists(Base):
session.commit() session.commit()
return pl.id return pl.id
@staticmethod
def get_last_used():
"""
Return a list of playlists marked "loaded", ordered by loaded date.
"""
return (
session.query(Playlists)
.filter(Playlists.loaded == True)
.order_by(Playlists.last_used.desc())
).all()
@staticmethod @staticmethod
def get_all_playlists(): def get_all_playlists():
"Returns a list of (id, name) of all playlists" "Returns a list of (id, name) of all playlists"

View File

@ -3,6 +3,7 @@ import threading
import vlc import vlc
from config import Config from config import Config
from datetime import datetime
from time import sleep from time import sleep
from log import DEBUG, ERROR from log import DEBUG, ERROR
@ -14,6 +15,7 @@ class Music:
""" """
def __init__(self, max_volume=100): def __init__(self, max_volume=100):
self.current_track_start_time = None
self.fading = False self.fading = False
self.VLC = vlc.Instance() self.VLC = vlc.Instance()
self.player = None self.player = None
@ -99,6 +101,7 @@ class Music:
self.player = self.VLC.media_player_new(path) self.player = self.VLC.media_player_new(path)
self.player.audio_set_volume(self.max_volume) self.player.audio_set_volume(self.max_volume)
self.player.play() self.player.play()
self.current_track_start_time = datetime.now()
def playing(self): def playing(self):
""" """

View File

@ -1,27 +1,33 @@
#!/usr/bin/env python #!/usr/bin/env python
import os
import sys import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
from log import DEBUG, EXCEPTION from log import DEBUG, EXCEPTION
from PyQt5 import Qt
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QApplication, QApplication,
QDialog,
QFileDialog, QFileDialog,
QInputDialog, QInputDialog,
QLabel, QListWidgetItem,
QMainWindow, QMainWindow,
QMessageBox, QMessageBox,
) )
import helpers import helpers
import music
from config import Config from config import Config
from model import Settings from model import Playdates, Playlists, Settings, Tracks
from songdb import add_path_to_db
from playlists import Playlist from playlists import Playlist
from songdb import add_path_to_db
from ui.dlg_search_database_ui import Ui_Dialog
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist
from ui.main_window_ui import Ui_MainWindow from ui.main_window_ui import Ui_MainWindow
@ -36,34 +42,20 @@ class Window(QMainWindow, Ui_MainWindow):
self.connect_signals_slots() self.connect_signals_slots()
self.disable_play_next_controls() self.disable_play_next_controls()
self.music = music.Music()
self.current_track = None
self.next_track = None
self.previous_track = None
self.previous_track_position = None
self.menuTest.menuAction().setVisible(Config.TESTMODE) self.menuTest.menuAction().setVisible(Config.TESTMODE)
self.set_main_window_size()
record = Settings.get_int("mainwindow_x") self.current_playlist = self.tabPlaylist.currentWidget
x = record.f_int or 1
record = Settings.get_int("mainwindow_y")
y = record.f_int or 1
record = Settings.get_int("mainwindow_width")
width = record.f_int or 1599
record = Settings.get_int("mainwindow_height")
height = record.f_int or 981
self.setGeometry(x, y, width, height)
# self.playlist.set_column_widths() self.load_last_playlists()
self.enable_play_next_controls()
# Hard code to the first playlist for now self.timer.start(Config.TIMER_MS)
# TODO
# self.playlist = Playlist()
# self.playlist.load_playlist(1)
# self.tabPlaylist.addTab(self.playlist, "Default")
# self.playlist.load_playlist(1)
# self.update_headers()
# 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)
def add_file(self): def add_file(self):
dlg = QFileDialog() dlg = QFileDialog()
@ -75,12 +67,28 @@ class Window(QMainWindow, Ui_MainWindow):
if dlg.exec_(): if dlg.exec_():
for fname in dlg.selectedFiles(): for fname in dlg.selectedFiles():
track = add_path_to_db(fname) track = add_path_to_db(fname)
self.playlist.add_to_playlist(track) self.current_playlist().add_to_playlist(track)
def set_main_window_size(self):
record = Settings.get_int("mainwindow_x")
x = record.f_int or 1
record = Settings.get_int("mainwindow_y")
y = record.f_int or 1
record = Settings.get_int("mainwindow_width")
width = record.f_int or 1599
record = Settings.get_int("mainwindow_height")
height = record.f_int or 981
self.setGeometry(x, y, width, height)
def clear_selection(self):
if self.current_playlist():
self.current_playlist().clearSelection()
def closeEvent(self, event): def closeEvent(self, event):
"Don't allow window to close when a track is playing" "Don't allow window to close when a track is playing"
if self.playlist.music.playing(): if self.music.playing():
DEBUG("closeEvent() ignored as music is playing") DEBUG("closeEvent() ignored as music is playing")
event.ignore() event.ignore()
# TODO notify user # TODO notify user
@ -107,8 +115,7 @@ class Window(QMainWindow, Ui_MainWindow):
def connect_signals_slots(self): def connect_signals_slots(self):
self.actionAdd_file.triggered.connect(self.add_file) self.actionAdd_file.triggered.connect(self.add_file)
# self.action_Clear_selection.triggered.connect( self.action_Clear_selection.triggered.connect(self.clear_selection)
# self.playlist.clearSelection)
self.actionFade.triggered.connect(self.fade) self.actionFade.triggered.connect(self.fade)
self.actionNewPlaylist.triggered.connect(self.create_playlist) self.actionNewPlaylist.triggered.connect(self.create_playlist)
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
@ -126,8 +133,9 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnPrevious.clicked.connect(self.play_previous) self.btnPrevious.clicked.connect(self.play_previous)
self.btnSetNext.clicked.connect(self.set_next_track) self.btnSetNext.clicked.connect(self.set_next_track)
self.btnSkipNext.clicked.connect(self.play_next) self.btnSkipNext.clicked.connect(self.play_next)
# self.btnStop.clicked.connect(self.playlist.stop) self.btnStop.clicked.connect(self.stop)
self.spnVolume.valueChanged.connect(self.change_volume) self.spnVolume.valueChanged.connect(self.change_volume)
self.tabPlaylist.currentChanged.connect(self.tab_change)
self.timer.timeout.connect(self.tick) self.timer.timeout.connect(self.tick)
@ -140,14 +148,14 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100) dlg.resize(500, 100)
ok = dlg.exec() ok = dlg.exec()
if ok: if ok:
self.playlist.create_playlist(dlg.textValue()) self.current_playlist().create_playlist(dlg.textValue())
def change_volume(self, volume): def change_volume(self, volume):
"Change player maximum volume" "Change player maximum volume"
DEBUG(f"change_volume({volume})") DEBUG(f"change_volume({volume})")
self.playlist.music.set_volume(volume) self.music.set_volume(volume)
def disable_play_next_controls(self): def disable_play_next_controls(self):
DEBUG("disable_play_next_controls()") DEBUG("disable_play_next_controls()")
@ -158,8 +166,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionPlay_next.setEnabled(True) self.actionPlay_next.setEnabled(True)
def fade(self): def fade(self):
self.playlist.fade() "Fade currently playing track"
self.enable_play_next_controls()
if not self.current_track:
return
self.previous_track = self.current_track
self.previous_track_position = self.music.fade()
def insert_note(self): def insert_note(self):
"Add non-track row to playlist" "Add non-track row to playlist"
@ -170,21 +183,89 @@ class Window(QMainWindow, Ui_MainWindow):
dlg.resize(500, 100) dlg.resize(500, 100)
ok = dlg.exec() ok = dlg.exec()
if ok: if ok:
self.playlist.add_note(dlg.textValue()) self.current_playlist().add_note(dlg.textValue())
def load_last_playlists(self):
"Load the playlists that we loaded at end of last session"
for p in Playlists.get_last_used():
playlist = Playlist()
playlist.load_playlist(p.id)
last_tab = self.tabPlaylist.addTab(playlist, p.name)
# Set last tab as active
self.tabPlaylist.setCurrentIndex(last_tab)
# Get next track
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
def play_next(self): def play_next(self):
self.playlist.play_next() """
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
DEBUG(
"play_next(), "
f"next_track={self.next_track.title if self.next_track else None} "
"current_track="
f"{self.current_track.title if self.current_track else None}"
)
# 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
next_track_id = self.current_playlist().started_playing_next()
self.next_track = Tracks.get_track(next_track_id)
# Check we can read it
if not os.access(self.next_track.path, os.R_OK):
self.show_warning(
"Can't read next track",
self.next_track.path)
# 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.current_playlist().mark_track_played(self.current_track.id)
self.disable_play_next_controls() self.disable_play_next_controls()
self.update_headers() self.update_headers()
# Set time clocks # Set time clocks
now = datetime.now() now = datetime.now()
self.label_start_tod.setText(now.strftime("%H:%M:%S")) self.label_start_tod.setText(now.strftime("%H:%M:%S"))
silence_at = self.playlist.get_current_silence_at() silence_at = self.current_track.silence_at
silence_time = now + timedelta(milliseconds=silence_at) silence_time = now + timedelta(milliseconds=silence_at)
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S")) self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
self.label_fade_length.setText(helpers.ms_to_mmss( self.label_fade_length.setText(helpers.ms_to_mmss(
silence_at - self.playlist.get_current_fade_at() silence_at - self.current_track.fade_at
)) ))
def play_previous(self): def play_previous(self):
@ -193,16 +274,27 @@ class Window(QMainWindow, Ui_MainWindow):
# TODO # TODO
pass pass
def playlist_changed(self):
"The playlist has changed (probably because the user changed tabs)"
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
def search_database(self): def search_database(self):
self.playlist.search_database() dlg = DbDialog(self)
dlg.exec()
def select_playlist(self): def select_playlist(self):
self.playlist.select_playlist() dlg = SelectPlaylistDialog(self)
dlg.exec()
def set_next_track(self): def set_next_track(self):
"Set selected track as next" "Set selected track as next"
self.playlist.set_selected_as_next() next_track_id = self.current_playlist().set_selected_as_next()
if next_track_id:
self.next_track = Tracks.get_track(next_track_id)
self.update_headers() self.update_headers()
def show_warning(self, title, msg): def show_warning(self, title, msg):
@ -210,37 +302,40 @@ class Window(QMainWindow, Ui_MainWindow):
QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel) QMessageBox.warning(None, title, msg, buttons=QMessageBox.Cancel)
def stop(self):
"Stop playing immediately"
self.previous_track = self.current_track
self.previous_track_position = self.music.stop()
def tab_change(self):
"User has changed tabs, so refresh next track"
self.next_track = Tracks.get_track(
self.current_playlist().get_next_track_id())
self.update_headers()
def test_function(self): def test_function(self):
"Placeholder for test function" "Placeholder for test function"
import ipdb import ipdb
ipdb.set_trace() ipdb.set_trace()
self.playlist = Playlist(parent=self.tabPlaylist)
self.tabPlaylist.addTab(self.playlist, "Default")
self.playlist.load_playlist(1)
self.playlist2 = Playlist(parent=self.tabPlaylist)
self.tabPlaylist.addTab(self.playlist2, "List 2")
self.playlist2.load_playlist(2)
def test_skip_to_end(self): def test_skip_to_end(self):
"Skip current track to 1 second before silence" "Skip current track to 1 second before silence"
if not self.playlist.music.playing(): if not self.playing():
return return
self.playlist.music.set_position( self.music.set_position(self.get_current_silence_at() - 1000)
self.playlist.get_current_silence_at() - 1000
)
def test_skip_to_fade(self): def test_skip_to_fade(self):
"Skip current track to 1 second before fade" "Skip current track to 1 second before fade"
if not self.playlist.music.playing(): if not self.music.playing():
return return
self.playlist.music.set_position( self.music.set_position(self.get_current_fade_at() - 1000)
self.playlist.get_current_fade_at() - 1000
)
def tick(self): def tick(self):
""" """
@ -262,19 +357,17 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.even_tick: if not self.even_tick:
return return
if self.playlist.music.playing(): if self.music.playing():
self.playing = True self.playing = True
playtime = self.playlist.music.get_playtime() playtime = self.music.get_playtime()
time_to_fade = (self.playlist.get_current_fade_at() - playtime) time_to_fade = (self.current_track.fade_at - playtime)
time_to_silence = ( time_to_silence = (self.current_track.silence_at - playtime)
self.playlist.get_current_silence_at() - playtime time_to_end = (self.current_track.duration - playtime)
)
time_to_end = (self.playlist.get_current_duration() - playtime)
# Elapsed time # Elapsed time
if time_to_end < 500: if time_to_end < 500:
self.label_elapsed_timer.setText( self.label_elapsed_timer.setText(
helpers.ms_to_mmss(self.playlist.get_current_duration()) helpers.ms_to_mmss(self.current_track.duration)
) )
else: else:
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime)) self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
@ -308,27 +401,136 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_end_timer.setText("00:00") self.label_end_timer.setText("00:00")
self.frame_silent.setStyleSheet("") self.frame_silent.setStyleSheet("")
self.playing = False self.playing = False
self.playlist.music_ended() self.previous_track = self.current_track
self.previous_track_position = 0
self.current_playlist().stopped_playing()
self.update_headers() self.update_headers()
def update_headers(self): def update_headers(self):
"Update last / current / next track headers" "Update last / current / next track headers"
self.previous_track.setText( try:
f"{self.playlist.get_previous_title()} - " self.hdrPreviousTrack.setText(
f"{self.playlist.get_previous_artist()}" f"{self.previous_track.title} - "
) f"{self.previous_track.artist}"
self.current_track.setText( )
f"{self.playlist.get_current_title()} - " except AttributeError:
f"{self.playlist.get_current_artist()}" self.hdrPreviousTrack.setText("")
)
self.next_track.setText(
f"{self.playlist.get_next_title()} - "
f"{self.playlist.get_next_artist()}"
)
def update_statusbar(self): try:
pass self.hdrCurrentTrack.setText(
f"{self.current_track.title} - "
f"{self.current_track.artist}"
)
except AttributeError:
self.hdrCurrentTrack.setText("")
try:
self.hdrNextTrack.setText(
f"{self.next_track.title} - "
f"{self.next_track.artist}"
)
except AttributeError:
self.hdrNextTrack.setText("")
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.double_click)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
self.ui.btnClose.clicked.connect(self.close)
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 add_selected(self):
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track_id = item.data(Qt.UserRole)
self.add_track(track_id)
def add_selected_and_close(self):
self.add_selected()
self.close()
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 double_click(self, entry):
track_id = entry.data(Qt.UserRole)
self.add_track(track_id)
# Select search text to make it easier for next search
self.select_searchtext()
def add_track(self, track_id):
track = Tracks.track_from_id(track_id)
self.parent().add_to_playlist(track)
# Select search text to make it easier for next search
self.select_searchtext()
def select_searchtext(self):
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
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.list_doubleclick)
self.ui.buttonBox.accepted.connect(self.open)
self.ui.buttonBox.rejected.connect(self.close)
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 list_doubleclick(self, entry):
plid = entry.data(Qt.UserRole)
self.parent().load_playlist(plid)
self.close()
def open(self):
if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem()
plid = item.data(Qt.UserRole)
self.parent().load_playlist(plid)
self.close()
def main(): def main():

View File

@ -5,27 +5,19 @@ from PyQt5.QtGui import QColor, QDropEvent
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAbstractItemView,
QApplication,
QDialog,
QHBoxLayout,
QListWidgetItem,
QMenu, QMenu,
QMessageBox, QMessageBox,
QTableWidget, QTableWidget,
QTableWidgetItem, QTableWidgetItem,
QWidget,
) )
import helpers import helpers
import music
import os import os
from config import Config 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, 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):
@ -67,6 +59,9 @@ class Playlist(QTableWidget):
item = QtWidgets.QTableWidgetItem() item = QtWidgets.QTableWidgetItem()
self.setHorizontalHeaderItem(6, item) self.setHorizontalHeaderItem(6, item)
self.horizontalHeader().setMinimumSectionSize(0) self.horizontalHeader().setMinimumSectionSize(0)
self._set_column_widths()
self.setDragEnabled(True) self.setDragEnabled(True)
self.setAcceptDrops(True) self.setAcceptDrops(True)
self.viewport().setAcceptDrops(True) self.viewport().setAcceptDrops(True)
@ -85,13 +80,7 @@ class Playlist(QTableWidget):
self.customContextMenuRequested.connect(self._context_menu) self.customContextMenuRequested.connect(self._context_menu)
self.viewport().installEventFilter(self) self.viewport().installEventFilter(self)
self.music = music.Music() self.current_track_start_time = None
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 = [] self.played_tracks = []
# ########## Events ########## # ########## Events ##########
@ -187,7 +176,7 @@ class Playlist(QTableWidget):
row = self.rowCount() row = self.rowCount()
DEBUG(f"playlist.add_note(): row={row}") DEBUG(f"playlist.add_note(): row={row}")
note = Notes.add_note(self.playlist_id, row, text) note = Notes.add_note(self.id, row, text)
self.add_to_playlist(note, row=row) self.add_to_playlist(note, row=row)
def add_to_playlist(self, data, repaint=True, row=None): def add_to_playlist(self, data, repaint=True, row=None):
@ -269,68 +258,10 @@ class Playlist(QTableWidget):
new_id = Playlists.new(name) new_id = Playlists.new(name)
self.load_playlist(new_id) self.load_playlist(new_id)
def fade(self): def get_next_track_id(self):
"Fade currently playing track"
if not self.current_track: next_row = self._meta_get_next()
return return self._get_row_id(next_row)
self.previous_track = self.current_track
self.previous_track_position = 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_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_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 load_playlist(self, plid): def load_playlist(self, plid):
""" """
@ -344,9 +275,6 @@ class Playlist(QTableWidget):
DEBUG(f"load_playlist(plid={plid})") DEBUG(f"load_playlist(plid={plid})")
p = Playlists.get_playlist_by_id(plid) p = Playlists.get_playlist_by_id(plid)
self.playlist_id = plid
self.playlist_name = p.name
# TODO 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
@ -366,115 +294,22 @@ class Playlist(QTableWidget):
for item in sorted(data, key=lambda x: x[0]): for item in sorted(data, key=lambda x: x[0]):
self.add_to_playlist(item[1], repaint=False) self.add_to_playlist(item[1], repaint=False)
# Set next track if we don't have one already set # Set next track for this playlist
if not self.next_track: notes_rows = self._meta_get_notes()
notes_rows = self._meta_get_notes() for row in range(self.rowCount()):
for row in range(self.rowCount()): if row in notes_rows:
if row in notes_rows: continue
continue self._meta_set_next(row)
self._cue_next_track(row) break
break
else:
self._repaint()
# Scroll to top # Scroll to top
scroll_to = self.item(0, self.COL_INDEX) scroll_to = self.item(0, self.COL_INDEX)
self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop)
def music_ended(self):
"Update display"
self.previous_track = self.current_track
self.previous_track_position = 0
self._meta_clear_current()
self._repaint() self._repaint()
def play_next(self): def mark_track_played(self, track_id):
""" self.played_tracks.append(track_id)
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
DEBUG(
"playlist.play_next(), "
f"next_track={self.next_track.title if self.next_track else None} "
"current_track="
f"{self.current_track.title if self.current_track else None}"
)
# 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)
self.current_track.start_time = datetime.now()
# Update metadata
self._meta_set_current(self._meta_get_next())
# Set up metadata for next track in playlist if there is one.
current_row = self._meta_get_current()
if current_row is not None:
start = current_row + 1
else:
start = 0
notes_rows = self._meta_get_notes()
for row in range(start, self.rowCount()):
if row in notes_rows:
continue
self._cue_next_track(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._repaint()
def search_database(self):
dlg = DbDialog(self)
dlg.exec()
def select_playlist(self):
dlg = SelectPlaylistDialog(self)
dlg.exec()
def set_column_widths(self):
# Column widths from settings
for column in range(self.columnCount()):
# Only show column 0 in test mode
if (column == 0 and not Config.TESTMODE):
self.setColumnWidth(0, 0)
else:
name = f"playlist_col_{str(column)}_width"
record = Settings.get_int(name)
if record.f_int is not None:
print("setting column width")
self.setColumnWidth(column, record.f_int)
def set_selected_as_next(self): def set_selected_as_next(self):
""" """
@ -484,13 +319,21 @@ class Playlist(QTableWidget):
if not self.selectionModel().hasSelection(): if not self.selectionModel().hasSelection():
return return
self._set_next(self.currentRow()) return self._set_next(self.currentRow())
def stop(self): def started_playing_next(self):
"Stop playing immediately" """
Update current track to be what was next, and determine next track.
Return next track_id.
"""
self.previous_track = self.current_track self.current_track_start_time = datetime.now()
self.previous_track_position = self.music.stop() self._meta_set_current(self._meta_get_next())
return self._mark_next_track()
def stopped_playing(self):
self._meta_clear_current()
self.current_track_start_time = None
# ########## Internally called functions ########## # ########## Internally called functions ##########
@ -515,27 +358,6 @@ class Playlist(QTableWidget):
self.menu.exec_(self.mapToGlobal(pos)) self.menu.exec_(self.mapToGlobal(pos))
def _cue_next_track(self, row):
"""
Set the passed row as the next track to play
"""
track_id = self._get_row_id(row)
if not track_id:
return
self._meta_set_next(row)
if not self.next_track or self.next_track.id != track_id:
self.next_track = Tracks.get_track(track_id)
# Check we can read it
if not self._can_read_track(self.next_track):
self.parent().parent().show_warning(
"Can't read next track",
self.next_track.path)
self._repaint()
def _delete_row(self, row): def _delete_row(self, row):
"Delete row" "Delete row"
@ -564,7 +386,7 @@ class Playlist(QTableWidget):
if row in self._meta_get_notes(): if row in self._meta_get_notes():
Notes.delete_note(id) Notes.delete_note(id)
else: else:
PlaylistTracks.remove_track(self.playlist_id, row) PlaylistTracks.remove_track(self.id, row)
self.removeRow(row) self.removeRow(row)
self._repaint() self._repaint()
@ -610,13 +432,32 @@ class Playlist(QTableWidget):
return False return False
elif rect.bottom() - pos.y() < margin: elif rect.bottom() - pos.y() < margin:
return True return True
# noinspection PyTypeChecker
return ( return (
rect.contains(pos, True) and not rect.contains(pos, True) and not
(int(self.model().flags(index)) & Qt.ItemIsDropEnabled) (int(self.model().flags(index)) & Qt.ItemIsDropEnabled)
and pos.y() >= rect.center().y() # noqa W503 and pos.y() >= rect.center().y() # noqa W503
) )
def _mark_next_track(self):
"Set up metadata for next track in playlist if there is one."
current_row = self._meta_get_current()
if current_row is not None:
start = current_row + 1
else:
start = 0
notes_rows = self._meta_get_notes()
for row in range(start, self.rowCount()):
if row in notes_rows:
continue
self._meta_set_next(row)
break
self._repaint()
track_id = self._get_row_id(row)
return track_id
def _meta_clear(self, row): def _meta_clear(self, row):
"Clear metadata for row" "Clear metadata for row"
@ -711,26 +552,27 @@ class Playlist(QTableWidget):
title = "" title = ""
DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})") DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})")
if row is None: if row is None:
raise ValueError(f"_meta_set() with row=None") raise ValueError("_meta_set() with row=None")
self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata)
def _set_next(self, row): def _set_next(self, row):
""" """
If passed row is track row, set that track as the next track to If passed row is track row, set that track as the next track to
be played and return True. Otherwise return False. be played and return track_id. Otherwise return None.
""" """
DEBUG(f"_set_next({row})") DEBUG(f"_set_next({row})")
if row in self._meta_get_notes(): if row in self._meta_get_notes():
return False return None
if self.item(row, self.COL_INDEX): if self.item(row, self.COL_INDEX):
self._cue_next_track(row) self._meta_set_next(row)
return True self._repaint()
return self._get_row_id(row)
return False else:
return None
def _repaint(self, clear_selection=True): def _repaint(self, clear_selection=True):
"Set row colours, fonts, etc, and save playlist" "Set row colours, fonts, etc, and save playlist"
@ -764,10 +606,10 @@ class Playlist(QTableWidget):
elif row == current: elif row == current:
# Set start time # Set start time
self._set_row_time(row, self.current_track.start_time) self._set_row_time(row, self.current_track_start_time)
# Calculate next_start_time # Calculate next_start_time
next_start_time = self._calculate_next_start_time( next_start_time = self._calculate_next_start_time(
row, self.current_track.start_time) row, self.current_track_start_time)
# Set colour # Set colour
self._set_row_colour(row, QColor( self._set_row_colour(row, QColor(
Config.COLOUR_CURRENT_PLAYLIST)) Config.COLOUR_CURRENT_PLAYLIST))
@ -775,10 +617,10 @@ class Playlist(QTableWidget):
self._set_row_bold(row) self._set_row_bold(row)
elif row == next: elif row == next:
# if there's a current row, set start time from that # if there's a current track playing, set start time from that
if self.current_track: if self.current_track_start_time:
start_time = self._calculate_next_start_time( start_time = self._calculate_next_start_time(
current, self.current_track.start_time) current, self.current_track_start_time)
else: else:
# No current track to base from, but don't change # No current track to base from, but don't change
# time if it's already set # time if it's already set
@ -813,9 +655,6 @@ class Playlist(QTableWidget):
# Don't dim unplayed tracks # Don't dim unplayed tracks
self._set_row_bold(row) self._set_row_bold(row)
# Headers might need updating
# TODO self.parent().parent().update_headers()
def _save_playlist(self): def _save_playlist(self):
""" """
Save playlist to database. We do this by correcting differences Save playlist to database. We do this by correcting differences
@ -827,7 +666,7 @@ class Playlist(QTableWidget):
times in one playlist and in multiple playlists. times in one playlist and in multiple playlists.
""" """
playlist = Playlists.get_playlist_by_id(self.playlist_id) playlist = Playlists.get_playlist_by_id(self.id)
# Notes first # Notes first
# Create dictionaries indexed by note_id # Create dictionaries indexed by note_id
@ -894,8 +733,7 @@ class Playlist(QTableWidget):
set(set(playlist_tracks.keys()) - set(database_tracks.keys())) set(set(playlist_tracks.keys()) - set(database_tracks.keys()))
): ):
DEBUG(f"_save_playlist(): row {row} missing from database") DEBUG(f"_save_playlist(): row {row} missing from database")
PlaylistTracks.add_track(self.playlist_id, playlist_tracks[row], PlaylistTracks.add_track(self.id, playlist_tracks[row], row)
row)
# Track rows to remove from database # Track rows to remove from database
for row in ( for row in (
@ -919,7 +757,21 @@ class Playlist(QTableWidget):
f"to track={playlist_tracks[row]}" f"to track={playlist_tracks[row]}"
) )
PlaylistTracks.update_row_track( PlaylistTracks.update_row_track(
self.playlist_id, row, playlist_tracks[row]) self.id, row, playlist_tracks[row])
def _set_column_widths(self):
# Column widths from settings
for column in range(self.columnCount()):
# Only show column 0 in test mode
if (column == 0 and not Config.TESTMODE):
self.setColumnWidth(0, 0)
else:
name = f"playlist_col_{str(column)}_width"
record = Settings.get_int(name)
if record.f_int is not None:
print("setting column width")
self.setColumnWidth(column, record.f_int)
def _set_row_bold(self, row, bold=True): def _set_row_bold(self, row, bold=True):
boldfont = QFont() boldfont = QFont()
@ -943,134 +795,3 @@ class Playlist(QTableWidget):
time_str = "" time_str = ""
item = QTableWidgetItem(time_str) item = QTableWidgetItem(time_str)
self.setItem(row, self.COL_START_TIME, item) self.setItem(row, self.COL_START_TIME, item)
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.double_click)
self.ui.btnAdd.clicked.connect(self.add_selected)
self.ui.btnAddClose.clicked.connect(self.add_selected_and_close)
self.ui.btnClose.clicked.connect(self.close)
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 add_selected(self):
if not self.ui.matchList.selectedItems():
return
item = self.ui.matchList.currentItem()
track_id = item.data(Qt.UserRole)
self.add_track(track_id)
def add_selected_and_close(self):
self.add_selected()
self.close()
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 double_click(self, entry):
track_id = entry.data(Qt.UserRole)
self.add_track(track_id)
# Select search text to make it easier for next search
self.select_searchtext()
def add_track(self, track_id):
track = Tracks.track_from_id(track_id)
self.parent().add_to_playlist(track)
# Select search text to make it easier for next search
self.select_searchtext()
def select_searchtext(self):
self.ui.searchString.selectAll()
self.ui.searchString.setFocus()
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.list_doubleclick)
self.ui.buttonBox.accepted.connect(self.open)
self.ui.buttonBox.rejected.connect(self.close)
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 list_doubleclick(self, entry):
plid = entry.data(Qt.UserRole)
self.parent().load_playlist(plid)
self.close()
def open(self):
if self.ui.lstPlaylists.selectedItems():
item = self.ui.lstPlaylists.currentItem()
plid = item.data(Qt.UserRole)
self.parent().load_playlist(plid)
self.close()
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_())

View File

@ -123,7 +123,7 @@ border: 1px solid rgb(85, 87, 83);</string>
<item> <item>
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="QLabel" name="previous_track"> <widget class="QLabel" name="hdrPreviousTrack">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>16</width> <width>16</width>
@ -152,7 +152,7 @@ border: 1px solid rgb(85, 87, 83);</string>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="current_track"> <widget class="QLabel" name="hdrCurrentTrack">
<property name="font"> <property name="font">
<font> <font>
<family>Sans</family> <family>Sans</family>
@ -169,7 +169,7 @@ border: 1px solid rgb(85, 87, 83);</string>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLabel" name="next_track"> <widget class="QLabel" name="hdrNextTrack">
<property name="font"> <property name="font">
<font> <font>
<family>Sans</family> <family>Sans</family>