From 51b2dd43e5d48e0f3d5ae9a005930df21122a7f3 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Thu, 29 Apr 2021 22:20:24 +0100 Subject: [PATCH] Tabbed playlists working --- app/model.py | 15 ++ app/music.py | 3 + app/musicmuster.py | 364 ++++++++++++++++++++++++++-------- app/playlists.py | 443 ++++++++---------------------------------- app/ui/main_window.ui | 6 +- 5 files changed, 386 insertions(+), 445 deletions(-) diff --git a/app/model.py b/app/model.py index b9e5491..6692b32 100644 --- a/app/model.py +++ b/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" diff --git a/app/music.py b/app/music.py index 1b6b88b..dd9733e 100644 --- a/app/music.py +++ b/app/music.py @@ -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): """ diff --git a/app/musicmuster.py b/app/musicmuster.py index eb006ca..e82f47d 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -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(): diff --git a/app/playlists.py b/app/playlists.py index dbe6cb9..d6d23c9 100644 --- a/app/playlists.py +++ b/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_()) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 0920e65..9409a15 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -123,7 +123,7 @@ border: 1px solid rgb(85, 87, 83); - + 16 @@ -152,7 +152,7 @@ border: 1px solid rgb(85, 87, 83); - + Sans @@ -169,7 +169,7 @@ border: 1px solid rgb(85, 87, 83); - + Sans