Tabbed playlists working
This commit is contained in:
parent
ed2a766c80
commit
51b2dd43e5
15
app/model.py
15
app/model.py
@ -5,6 +5,7 @@ import sqlalchemy
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy import (
|
||||
Boolean,
|
||||
Column,
|
||||
DateTime,
|
||||
Float,
|
||||
@ -153,6 +154,8 @@ class Playlists(Base):
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=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",
|
||||
order_by="Notes.row",
|
||||
back_populates="playlist")
|
||||
@ -172,6 +175,18 @@ class Playlists(Base):
|
||||
session.commit()
|
||||
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
|
||||
def get_all_playlists():
|
||||
"Returns a list of (id, name) of all playlists"
|
||||
|
||||
@ -3,6 +3,7 @@ import threading
|
||||
import vlc
|
||||
|
||||
from config import Config
|
||||
from datetime import datetime
|
||||
from time import sleep
|
||||
|
||||
from log import DEBUG, ERROR
|
||||
@ -14,6 +15,7 @@ class Music:
|
||||
"""
|
||||
|
||||
def __init__(self, max_volume=100):
|
||||
self.current_track_start_time = None
|
||||
self.fading = False
|
||||
self.VLC = vlc.Instance()
|
||||
self.player = None
|
||||
@ -99,6 +101,7 @@ class Music:
|
||||
self.player = self.VLC.media_player_new(path)
|
||||
self.player.audio_set_volume(self.max_volume)
|
||||
self.player.play()
|
||||
self.current_track_start_time = datetime.now()
|
||||
|
||||
def playing(self):
|
||||
"""
|
||||
|
||||
@ -1,27 +1,33 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from log import DEBUG, EXCEPTION
|
||||
|
||||
from PyQt5 import Qt
|
||||
from PyQt5.QtCore import QTimer
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
QDialog,
|
||||
QFileDialog,
|
||||
QInputDialog,
|
||||
QLabel,
|
||||
QListWidgetItem,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
)
|
||||
|
||||
import helpers
|
||||
import music
|
||||
|
||||
from config import Config
|
||||
from model import Settings
|
||||
from songdb import add_path_to_db
|
||||
from model import Playdates, Playlists, Settings, Tracks
|
||||
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
|
||||
|
||||
|
||||
@ -36,34 +42,20 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.connect_signals_slots()
|
||||
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.set_main_window_size()
|
||||
|
||||
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)
|
||||
self.current_playlist = self.tabPlaylist.currentWidget
|
||||
|
||||
# self.playlist.set_column_widths()
|
||||
|
||||
# Hard code to the first playlist for now
|
||||
# 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)
|
||||
self.load_last_playlists()
|
||||
self.enable_play_next_controls()
|
||||
self.timer.start(Config.TIMER_MS)
|
||||
|
||||
def add_file(self):
|
||||
dlg = QFileDialog()
|
||||
@ -75,12 +67,28 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if dlg.exec_():
|
||||
for fname in dlg.selectedFiles():
|
||||
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):
|
||||
"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")
|
||||
event.ignore()
|
||||
# TODO notify user
|
||||
@ -107,8 +115,7 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
def connect_signals_slots(self):
|
||||
self.actionAdd_file.triggered.connect(self.add_file)
|
||||
# self.action_Clear_selection.triggered.connect(
|
||||
# self.playlist.clearSelection)
|
||||
self.action_Clear_selection.triggered.connect(self.clear_selection)
|
||||
self.actionFade.triggered.connect(self.fade)
|
||||
self.actionNewPlaylist.triggered.connect(self.create_playlist)
|
||||
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.btnSetNext.clicked.connect(self.set_next_track)
|
||||
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.tabPlaylist.currentChanged.connect(self.tab_change)
|
||||
|
||||
self.timer.timeout.connect(self.tick)
|
||||
|
||||
@ -140,14 +148,14 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
if ok:
|
||||
self.playlist.create_playlist(dlg.textValue())
|
||||
self.current_playlist().create_playlist(dlg.textValue())
|
||||
|
||||
def change_volume(self, volume):
|
||||
"Change player maximum volume"
|
||||
|
||||
DEBUG(f"change_volume({volume})")
|
||||
|
||||
self.playlist.music.set_volume(volume)
|
||||
self.music.set_volume(volume)
|
||||
|
||||
def disable_play_next_controls(self):
|
||||
DEBUG("disable_play_next_controls()")
|
||||
@ -158,8 +166,13 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
self.actionPlay_next.setEnabled(True)
|
||||
|
||||
def fade(self):
|
||||
self.playlist.fade()
|
||||
self.enable_play_next_controls()
|
||||
"Fade currently playing track"
|
||||
|
||||
if not self.current_track:
|
||||
return
|
||||
|
||||
self.previous_track = self.current_track
|
||||
self.previous_track_position = self.music.fade()
|
||||
|
||||
def insert_note(self):
|
||||
"Add non-track row to playlist"
|
||||
@ -170,21 +183,89 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
dlg.resize(500, 100)
|
||||
ok = dlg.exec()
|
||||
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):
|
||||
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.update_headers()
|
||||
|
||||
# Set time clocks
|
||||
now = datetime.now()
|
||||
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)
|
||||
self.label_silent_tod.setText(silence_time.strftime("%H:%M:%S"))
|
||||
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):
|
||||
@ -193,16 +274,27 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
# TODO
|
||||
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):
|
||||
self.playlist.search_database()
|
||||
dlg = DbDialog(self)
|
||||
dlg.exec()
|
||||
|
||||
def select_playlist(self):
|
||||
self.playlist.select_playlist()
|
||||
dlg = SelectPlaylistDialog(self)
|
||||
dlg.exec()
|
||||
|
||||
def set_next_track(self):
|
||||
"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()
|
||||
|
||||
def show_warning(self, title, msg):
|
||||
@ -210,37 +302,40 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
|
||||
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):
|
||||
"Placeholder for test function"
|
||||
|
||||
import ipdb
|
||||
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):
|
||||
"Skip current track to 1 second before silence"
|
||||
|
||||
if not self.playlist.music.playing():
|
||||
if not self.playing():
|
||||
return
|
||||
|
||||
self.playlist.music.set_position(
|
||||
self.playlist.get_current_silence_at() - 1000
|
||||
)
|
||||
self.music.set_position(self.get_current_silence_at() - 1000)
|
||||
|
||||
def test_skip_to_fade(self):
|
||||
"Skip current track to 1 second before fade"
|
||||
|
||||
if not self.playlist.music.playing():
|
||||
if not self.music.playing():
|
||||
return
|
||||
|
||||
self.playlist.music.set_position(
|
||||
self.playlist.get_current_fade_at() - 1000
|
||||
)
|
||||
self.music.set_position(self.get_current_fade_at() - 1000)
|
||||
|
||||
def tick(self):
|
||||
"""
|
||||
@ -262,19 +357,17 @@ class Window(QMainWindow, Ui_MainWindow):
|
||||
if not self.even_tick:
|
||||
return
|
||||
|
||||
if self.playlist.music.playing():
|
||||
if self.music.playing():
|
||||
self.playing = True
|
||||
playtime = self.playlist.music.get_playtime()
|
||||
time_to_fade = (self.playlist.get_current_fade_at() - playtime)
|
||||
time_to_silence = (
|
||||
self.playlist.get_current_silence_at() - playtime
|
||||
)
|
||||
time_to_end = (self.playlist.get_current_duration() - playtime)
|
||||
playtime = self.music.get_playtime()
|
||||
time_to_fade = (self.current_track.fade_at - playtime)
|
||||
time_to_silence = (self.current_track.silence_at - playtime)
|
||||
time_to_end = (self.current_track.duration - playtime)
|
||||
|
||||
# Elapsed time
|
||||
if time_to_end < 500:
|
||||
self.label_elapsed_timer.setText(
|
||||
helpers.ms_to_mmss(self.playlist.get_current_duration())
|
||||
helpers.ms_to_mmss(self.current_track.duration)
|
||||
)
|
||||
else:
|
||||
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.frame_silent.setStyleSheet("")
|
||||
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()
|
||||
|
||||
def update_headers(self):
|
||||
"Update last / current / next track headers"
|
||||
|
||||
self.previous_track.setText(
|
||||
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()}"
|
||||
)
|
||||
try:
|
||||
self.hdrPreviousTrack.setText(
|
||||
f"{self.previous_track.title} - "
|
||||
f"{self.previous_track.artist}"
|
||||
)
|
||||
except AttributeError:
|
||||
self.hdrPreviousTrack.setText("")
|
||||
|
||||
def update_statusbar(self):
|
||||
pass
|
||||
try:
|
||||
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():
|
||||
|
||||
443
app/playlists.py
443
app/playlists.py
@ -5,27 +5,19 @@ from PyQt5.QtGui import QColor, QDropEvent
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QDialog,
|
||||
QHBoxLayout,
|
||||
QListWidgetItem,
|
||||
QMenu,
|
||||
QMessageBox,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
import helpers
|
||||
import music
|
||||
import os
|
||||
|
||||
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
|
||||
from model import Notes, Playlists, PlaylistTracks, Settings, Tracks
|
||||
|
||||
|
||||
class Playlist(QTableWidget):
|
||||
@ -67,6 +59,9 @@ class Playlist(QTableWidget):
|
||||
item = QtWidgets.QTableWidgetItem()
|
||||
self.setHorizontalHeaderItem(6, item)
|
||||
self.horizontalHeader().setMinimumSectionSize(0)
|
||||
|
||||
self._set_column_widths()
|
||||
|
||||
self.setDragEnabled(True)
|
||||
self.setAcceptDrops(True)
|
||||
self.viewport().setAcceptDrops(True)
|
||||
@ -85,13 +80,7 @@ class Playlist(QTableWidget):
|
||||
self.customContextMenuRequested.connect(self._context_menu)
|
||||
self.viewport().installEventFilter(self)
|
||||
|
||||
self.music = music.Music()
|
||||
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.current_track_start_time = None
|
||||
self.played_tracks = []
|
||||
|
||||
# ########## Events ##########
|
||||
@ -187,7 +176,7 @@ class Playlist(QTableWidget):
|
||||
row = self.rowCount()
|
||||
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)
|
||||
|
||||
def add_to_playlist(self, data, repaint=True, row=None):
|
||||
@ -269,68 +258,10 @@ class Playlist(QTableWidget):
|
||||
new_id = Playlists.new(name)
|
||||
self.load_playlist(new_id)
|
||||
|
||||
def fade(self):
|
||||
"Fade currently playing track"
|
||||
def get_next_track_id(self):
|
||||
|
||||
if not self.current_track:
|
||||
return
|
||||
|
||||
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 ""
|
||||
next_row = self._meta_get_next()
|
||||
return self._get_row_id(next_row)
|
||||
|
||||
def load_playlist(self, plid):
|
||||
"""
|
||||
@ -344,9 +275,6 @@ class Playlist(QTableWidget):
|
||||
DEBUG(f"load_playlist(plid={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
|
||||
# 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]):
|
||||
self.add_to_playlist(item[1], repaint=False)
|
||||
|
||||
# Set next track if we don't have one already set
|
||||
if not self.next_track:
|
||||
notes_rows = self._meta_get_notes()
|
||||
for row in range(self.rowCount()):
|
||||
if row in notes_rows:
|
||||
continue
|
||||
self._cue_next_track(row)
|
||||
break
|
||||
else:
|
||||
self._repaint()
|
||||
# Set next track for this playlist
|
||||
notes_rows = self._meta_get_notes()
|
||||
for row in range(self.rowCount()):
|
||||
if row in notes_rows:
|
||||
continue
|
||||
self._meta_set_next(row)
|
||||
break
|
||||
|
||||
# Scroll to top
|
||||
scroll_to = self.item(0, self.COL_INDEX)
|
||||
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()
|
||||
|
||||
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
|
||||
|
||||
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 mark_track_played(self, track_id):
|
||||
self.played_tracks.append(track_id)
|
||||
|
||||
def set_selected_as_next(self):
|
||||
"""
|
||||
@ -484,13 +319,21 @@ class Playlist(QTableWidget):
|
||||
if not self.selectionModel().hasSelection():
|
||||
return
|
||||
|
||||
self._set_next(self.currentRow())
|
||||
return self._set_next(self.currentRow())
|
||||
|
||||
def stop(self):
|
||||
"Stop playing immediately"
|
||||
def started_playing_next(self):
|
||||
"""
|
||||
Update current track to be what was next, and determine next track.
|
||||
Return next track_id.
|
||||
"""
|
||||
|
||||
self.previous_track = self.current_track
|
||||
self.previous_track_position = self.music.stop()
|
||||
self.current_track_start_time = datetime.now()
|
||||
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 ##########
|
||||
|
||||
@ -515,27 +358,6 @@ class Playlist(QTableWidget):
|
||||
|
||||
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):
|
||||
"Delete row"
|
||||
|
||||
@ -564,7 +386,7 @@ class Playlist(QTableWidget):
|
||||
if row in self._meta_get_notes():
|
||||
Notes.delete_note(id)
|
||||
else:
|
||||
PlaylistTracks.remove_track(self.playlist_id, row)
|
||||
PlaylistTracks.remove_track(self.id, row)
|
||||
self.removeRow(row)
|
||||
|
||||
self._repaint()
|
||||
@ -610,13 +432,32 @@ class Playlist(QTableWidget):
|
||||
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 _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):
|
||||
"Clear metadata for row"
|
||||
|
||||
@ -711,26 +552,27 @@ class Playlist(QTableWidget):
|
||||
title = ""
|
||||
DEBUG(f"_meta_set(row={row}, title={title}, metadata={metadata})")
|
||||
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)
|
||||
|
||||
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.
|
||||
be played and return track_id. Otherwise return None.
|
||||
"""
|
||||
|
||||
DEBUG(f"_set_next({row})")
|
||||
|
||||
if row in self._meta_get_notes():
|
||||
return False
|
||||
return None
|
||||
|
||||
if self.item(row, self.COL_INDEX):
|
||||
self._cue_next_track(row)
|
||||
return True
|
||||
|
||||
return False
|
||||
self._meta_set_next(row)
|
||||
self._repaint()
|
||||
return self._get_row_id(row)
|
||||
else:
|
||||
return None
|
||||
|
||||
def _repaint(self, clear_selection=True):
|
||||
"Set row colours, fonts, etc, and save playlist"
|
||||
@ -764,10 +606,10 @@ class Playlist(QTableWidget):
|
||||
|
||||
elif row == current:
|
||||
# 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
|
||||
next_start_time = self._calculate_next_start_time(
|
||||
row, self.current_track.start_time)
|
||||
row, self.current_track_start_time)
|
||||
# Set colour
|
||||
self._set_row_colour(row, QColor(
|
||||
Config.COLOUR_CURRENT_PLAYLIST))
|
||||
@ -775,10 +617,10 @@ class Playlist(QTableWidget):
|
||||
self._set_row_bold(row)
|
||||
|
||||
elif row == next:
|
||||
# if there's a current row, set start time from that
|
||||
if self.current_track:
|
||||
# if there's a current track playing, set start time from that
|
||||
if self.current_track_start_time:
|
||||
start_time = self._calculate_next_start_time(
|
||||
current, self.current_track.start_time)
|
||||
current, self.current_track_start_time)
|
||||
else:
|
||||
# No current track to base from, but don't change
|
||||
# time if it's already set
|
||||
@ -813,9 +655,6 @@ class Playlist(QTableWidget):
|
||||
# Don't dim unplayed tracks
|
||||
self._set_row_bold(row)
|
||||
|
||||
# Headers might need updating
|
||||
# TODO self.parent().parent().update_headers()
|
||||
|
||||
def _save_playlist(self):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
|
||||
playlist = Playlists.get_playlist_by_id(self.playlist_id)
|
||||
playlist = Playlists.get_playlist_by_id(self.id)
|
||||
|
||||
# Notes first
|
||||
# Create dictionaries indexed by note_id
|
||||
@ -894,8 +733,7 @@ class Playlist(QTableWidget):
|
||||
set(set(playlist_tracks.keys()) - set(database_tracks.keys()))
|
||||
):
|
||||
DEBUG(f"_save_playlist(): row {row} missing from database")
|
||||
PlaylistTracks.add_track(self.playlist_id, playlist_tracks[row],
|
||||
row)
|
||||
PlaylistTracks.add_track(self.id, playlist_tracks[row], row)
|
||||
|
||||
# Track rows to remove from database
|
||||
for row in (
|
||||
@ -919,7 +757,21 @@ class Playlist(QTableWidget):
|
||||
f"to track={playlist_tracks[row]}"
|
||||
)
|
||||
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):
|
||||
boldfont = QFont()
|
||||
@ -943,134 +795,3 @@ class Playlist(QTableWidget):
|
||||
time_str = ""
|
||||
item = QTableWidgetItem(time_str)
|
||||
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_())
|
||||
|
||||
@ -123,7 +123,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="previous_track">
|
||||
<widget class="QLabel" name="hdrPreviousTrack">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>16</width>
|
||||
@ -152,7 +152,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="current_track">
|
||||
<widget class="QLabel" name="hdrCurrentTrack">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
@ -169,7 +169,7 @@ border: 1px solid rgb(85, 87, 83);</string>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="next_track">
|
||||
<widget class="QLabel" name="hdrNextTrack">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Sans</family>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user