diff --git a/app/config.py b/app/config.py index 6222768..a4bc57d 100644 --- a/app/config.py +++ b/app/config.py @@ -3,6 +3,7 @@ import os class Config(object): + AUDACITY_COMMAND = "/usr/bin/audacity" AUDIO_SEGMENT_CHUNK_SIZE = 10 COLOUR_CURRENT_HEADER = "#d4edda" COLOUR_CURRENT_PLAYLIST = "#7eca8f" @@ -19,8 +20,17 @@ class Config(object): COLOUR_PREVIOUS_HEADER = "#f8d7da" COLOUR_UNREADABLE = "#dc3545" COLOUR_WARNING_TIMER = "#ffc107" + COLUMN_NAME_ARTIST = "Artist" + COLUMN_NAME_AUTOPLAY = "A" + COLUMN_NAME_END_TIME = "End" + COLUMN_NAME_LAST_PLAYED = "Last played" + COLUMN_NAME_LEADING_SILENCE = "Gap" + COLUMN_NAME_LENGTH = "Length" + COLUMN_NAME_START_TIME = "Start" + COLUMN_NAME_TITLE = "Title" DBFS_FADE = -12 DBFS_SILENCE = -50 + DEFAULT_COLUMN_WIDTH = 200 DEFAULT_IMPORT_DIRECTORY = "/home/kae/Nextcloud/tmp" DEFAULT_OUTPUT_DIRECTORY = "/home/kae/music/Singles" DISPLAY_SQL = False diff --git a/app/helpers.py b/app/helpers.py index 892c7d0..6c54e3f 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -1,7 +1,7 @@ import os import psutil -from app.config import Config +from config import Config from datetime import datetime from pydub import AudioSegment from mutagen.flac import FLAC @@ -10,6 +10,14 @@ from PyQt5.QtWidgets import QMessageBox from tinytag import TinyTag +def ask_yes_no(title, question): + """Ask question; return True for yes, False for no""" + + buttonResponse = QMessageBox.question( + self, title, question, QMessageBox.Yes | QMessageBox.No, + QMessageBox.No) + + def fade_point(audio_segment, fade_threshold=0, chunk_size=Config.AUDIO_SEGMENT_CHUNK_SIZE): """ diff --git a/app/models.py b/app/models.py index 0f00f31..b30a20e 100644 --- a/app/models.py +++ b/app/models.py @@ -25,39 +25,33 @@ from sqlalchemy.orm import backref, relationship, sessionmaker, scoped_session from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound -from app.config import Config -from app.helpers import ( +from config import Config +from helpers import ( fade_point, get_audio_segment, leading_silence, show_warning, trailing_silence, ) -from app.log import DEBUG, ERROR +from log import DEBUG, ERROR # Create session at the global level as per # https://docs.sqlalchemy.org/en/13/orm/session_basics.html -Base = declarative_base() -Session = scoped_session(sessionmaker()) +engine = sqlalchemy.create_engine( + f"{Config.MYSQL_CONNECT}?charset=utf8", + encoding='utf-8', + echo=Config.DISPLAY_SQL, + pool_pre_ping=True) + +Session = scoped_session(sessionmaker(bind=engine)) + +Base: DeclarativeMeta = declarative_base() +Base.metadata.create_all(engine) def db_init(): - # Set up database connection - - global Session - - engine = sqlalchemy.create_engine( - f"{Config.MYSQL_CONNECT}?charset=utf8", - encoding='utf-8', - echo=Config.DISPLAY_SQL, - pool_pre_ping=True) - - Session.configure(bind=engine) - Base.metadata.create_all(engine) - - # Create a Session factory - Session = sessionmaker(bind=engine) + return # Database classes diff --git a/app/musicmuster.py b/app/musicmuster.py index 8cdcf74..c497dff 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -10,7 +10,7 @@ import urllib.parse from datetime import datetime, timedelta from log import DEBUG, EXCEPTION -from PyQt5.QtCore import Qt, QTimer, QUrl +from PyQt5.QtCore import QProcess, Qt, QTimer, QUrl from PyQt5.QtGui import QColor, QFontMetrics, QPainter from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( @@ -43,16 +43,19 @@ class ElideLabel(QLabel): """ def paintEvent(self, event): + #TODO: V2 check painter = QPainter(self) metrics = QFontMetrics(self.font()) elided = metrics.elidedText(self.text(), Qt.ElideRight, self.width()) painter.drawText(self.rect(), self.alignment(), elided) + #TODO: V2 check class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): + #TODO: V2 check super().__init__(parent) self.setupUi(self) @@ -85,6 +88,7 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.start(Config.TIMER_MS) def add_file(self): + #TODO: V2 check dlg = QFileDialog() dlg.setFileMode(QFileDialog.ExistingFiles) dlg.setViewMode(QFileDialog.Detail) @@ -98,9 +102,10 @@ class Window(QMainWindow, Ui_MainWindow): # Add to playlist on screen # If we don't specify "repaint=False", playlist will # also be saved to database - self.visible_playlist_tab().insert_track(session, track) + self.visible_playlist_tab()._insert_track(session, track) def set_main_window_size(self): + #TODO: V2 check "Set size of window from database" with Session() as session: @@ -115,18 +120,22 @@ class Window(QMainWindow, Ui_MainWindow): self.setGeometry(x, y, width, height) def check_audacity(self): - "Warn user if Audacity not running" + #TODO: V2 check + "Offer to run Audacity if not running" if "audacity" in [i.name() for i in psutil.process_iter()]: return - helpers.show_warning("Audacity check", "Audacity is not running") + if helpers.ask_yes_no("Audacity not running", "Start Audacity?"): + QProcess.startDetached(Config.AUDACITY_COMMAND, []) def clear_selection(self): + #TODO: V2 check if self.visible_playlist_tab(): self.visible_playlist_tab().clearSelection() def closeEvent(self, event): + #TODO: V2 check "Don't allow window to close when a track is playing" if self.music.playing(): @@ -165,6 +174,7 @@ class Window(QMainWindow, Ui_MainWindow): event.accept() def connect_signals_slots(self): + #TODO: V2 check self.actionAdd_file.triggered.connect(self.add_file) self.actionAdd_note.triggered.connect(self.insert_note) self.action_Clear_selection.triggered.connect(self.clear_selection) @@ -202,6 +212,7 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.timeout.connect(self.tick) def create_playlist(self): + #TODO: V2 check "Create new playlist" dlg = QInputDialog(self) @@ -212,9 +223,10 @@ class Window(QMainWindow, Ui_MainWindow): if ok: with Session() as session: playlist_db = Playlists(session, dlg.textValue()) - self.load_playlist(session, playlist_db) + self.create_playlist_tab(session, playlist_db) def change_volume(self, volume): + #TODO: V2 check "Change player maximum volume" DEBUG(f"change_volume({volume})") @@ -222,9 +234,11 @@ class Window(QMainWindow, Ui_MainWindow): self.music.set_volume(volume) def close_playlist_tab(self): + #TODO: V2 check self.close_tab(self.tabPlaylist.currentIndex()) def close_tab(self, index): + #TODO: V2 check if hasattr(self.tabPlaylist.widget(index), 'is_playlist'): if self.tabPlaylist.widget(index) == ( self.current_track_playlist_tab): @@ -244,6 +258,7 @@ class Window(QMainWindow, Ui_MainWindow): self.tabPlaylist.removeTab(index) def create_note(self, session, text): + #TODO: V2 check """ Create note @@ -265,16 +280,19 @@ class Window(QMainWindow, Ui_MainWindow): return note def disable_play_next_controls(self): + #TODO: V2 check DEBUG("disable_play_next_controls()") self.actionPlay_next.setEnabled(False) self.statusbar.showMessage("Play controls: Disabled", 0) def enable_play_next_controls(self): + #TODO: V2 check DEBUG("enable_play_next_controls()") self.actionPlay_next.setEnabled(True) self.statusbar.showMessage("Play controls: Enabled", 0) def end_of_track_actions(self): + #TODO: V2 check "Clean up after track played" # Set self.playing to False so that tick() doesn't see @@ -301,6 +319,7 @@ class Window(QMainWindow, Ui_MainWindow): self.enable_play_next_controls() def ensure_info_tabs(self, title_list): + #TODO: V2 check """ Ensure we have info tabs for each of the passed titles """ @@ -336,6 +355,7 @@ class Window(QMainWindow, Ui_MainWindow): widget.setUrl(QUrl(url)) def export_playlist_tab(self): + #TODO: V2 check "Export the current playlist to an m3u file" if not self.visible_playlist_tab(): @@ -374,6 +394,7 @@ class Window(QMainWindow, Ui_MainWindow): ) def fade(self): + #TODO: V2 check "Fade currently playing track" DEBUG("musicmuster:fade()", True) @@ -385,6 +406,7 @@ class Window(QMainWindow, Ui_MainWindow): self.end_of_track_actions() def insert_note(self): + #TODO: V2 check "Add non-track row to playlist" dlg = QInputDialog(self) @@ -395,28 +417,29 @@ class Window(QMainWindow, Ui_MainWindow): if ok: with Session() as session: note = self.create_note(session, dlg.textValue()) - self.visible_playlist_tab().insert_note(session, note) + self.visible_playlist_tab()._insert_note(session, note) def load_last_playlists(self): + #TODO: V2 check "Load the playlists that we loaded at end of last session" with Session() as session: for playlist_db in Playlists.get_open(session): - self.load_playlist(session, playlist_db) + self.create_playlist_tab(session, playlist_db) - def load_playlist(self, session, playlist_db): + def create_playlist_tab(self, session, playlist_db): + #TODO: V2 check """ - Take the passed database object, create a playlist display, attach - the database object, get it populated and then add tab. + Take the passed database object, create a playlist tab and add tab + to display. """ - playlist_tab = PlaylistTab(self) - playlist_db.mark_open(session) - playlist_tab.populate(session, playlist_db) + playlist_tab = PlaylistTab(self, session, playlist_db) idx = self.tabPlaylist.addTab(playlist_tab, playlist_db.name) self.tabPlaylist.setCurrentIndex(idx) def move_selected(self): + #TODO: V2 check "Move selected rows to another playlist" # TODO needs refactoring @@ -465,6 +488,7 @@ class Window(QMainWindow, Ui_MainWindow): self.visible_playlist_tab().remove_rows(rows) def play_next(self): + #TODO: V2 check """ Play next track. @@ -545,35 +569,41 @@ class Window(QMainWindow, Ui_MainWindow): )) def search_database(self): + #TODO: V2 check with Session() as session: dlg = DbDialog(self, session) dlg.exec() def open_playlist(self): + #TODO: V2 check with Session() as session: playlist_dbs = Playlists.get_closed(session) dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs) dlg.exec() if dlg.plid: playlist_db = Playlists.get_by_id(session, dlg.plid) - self.load_playlist(session, playlist_db) + self.create_playlist_tab(session, playlist_db) def select_next_row(self): + #TODO: V2 check "Select next or first row in playlist" self.visible_playlist_tab().select_next_row() def select_played(self): + #TODO: V2 check "Select all played tracks in playlist" self.visible_playlist_tab().select_played_tracks() def select_previous_row(self): + #TODO: V2 check "Select previous or first row in playlist" self.visible_playlist_tab().select_previous_row() def set_next_track(self, next_track_id=None): + #TODO: V2 check "Set selected track as next" with Session() as session: @@ -609,11 +639,13 @@ class Window(QMainWindow, Ui_MainWindow): self.update_headers() def select_unplayed(self): + #TODO: V2 check "Select all unplayed tracks in playlist" self.visible_playlist_tab().select_unplayed_tracks() def set_tab_colour(self, widget, colour): + #TODO: V2 check """ Find the tab containing the widget and set the text colour """ @@ -622,6 +654,7 @@ class Window(QMainWindow, Ui_MainWindow): self.tabPlaylist.tabBar().setTabTextColor(idx, colour) def song_info_search(self): + #TODO: V2 check """ Open browser tabs for Wikipedia, searching for the first that exists of: @@ -644,6 +677,7 @@ class Window(QMainWindow, Ui_MainWindow): webbrowser.open(url, new=2) def stop(self): + #TODO: V2 check "Stop playing immediately" DEBUG("musicmuster.stop()") @@ -651,6 +685,7 @@ class Window(QMainWindow, Ui_MainWindow): self.stop_playing(fade=False) def stop_playing(self, fade=True): + #TODO: V2 check "Stop playing current track" DEBUG(f"musicmuster.stop_playing({fade=})", True) @@ -682,11 +717,13 @@ class Window(QMainWindow, Ui_MainWindow): self.update_headers() def test_function(self): + #TODO: V2 check "Placeholder for test function" pass def test_skip_to_end(self): + #TODO: V2 check "Skip current track to 1 second before silence" if not self.playing: @@ -695,6 +732,7 @@ class Window(QMainWindow, Ui_MainWindow): self.music.set_position(self.current_track.silence_at - 1000) def test_skip_to_fade(self): + #TODO: V2 check "Skip current track to 1 second before fade" if not self.music.playing(): @@ -703,6 +741,7 @@ class Window(QMainWindow, Ui_MainWindow): self.music.set_position(self.current_track.fade_at - 1000) def tick(self): + #TODO: V2 check """ Update screen @@ -775,6 +814,7 @@ class Window(QMainWindow, Ui_MainWindow): self.stop_playing() def update_headers(self): + #TODO: V2 check """ Update last / current / next track headers. Ensure a Wikipedia tab for each title. @@ -811,6 +851,7 @@ class Window(QMainWindow, Ui_MainWindow): class DbDialog(QDialog): def __init__(self, parent, session): + #TODO: V2 check super().__init__(parent) self.session = session self.ui = Ui_Dialog() @@ -830,6 +871,7 @@ class DbDialog(QDialog): self.resize(width, height) def __del__(self): + #TODO: V2 check record = Settings.get_int(self.session, "dbdialog_height") if record.f_int != self.height(): record.update(self.session, {'f_int': self.height()}) @@ -839,6 +881,7 @@ class DbDialog(QDialog): record.update(self.session, {'f_int': self.width()}) def add_selected(self): + #TODO: V2 check if not self.ui.matchList.selectedItems(): return @@ -847,10 +890,12 @@ class DbDialog(QDialog): self.add_track(track_id) def add_selected_and_close(self): + #TODO: V2 check self.add_selected() self.close() def radio_toggle(self): + #TODO: V2 check """ Handle switching between searching for artists and searching for titles @@ -860,6 +905,7 @@ class DbDialog(QDialog): self.chars_typed(self.ui.searchString.text()) def chars_typed(self, s): + #TODO: V2 check if len(s) > 0: if self.ui.radioTitle.isChecked(): matches = Tracks.search_titles(self.session, s) @@ -877,36 +923,42 @@ class DbDialog(QDialog): self.ui.matchList.addItem(t) def double_click(self, entry): + #TODO: V2 check 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): + #TODO: V2 check track = Tracks.get_by_id(self.session, track_id) # Add to playlist on screen # If we don't specify "repaint=False", playlist will # also be saved to database - self.parent().visible_playlist_tab().insert_track( + self.parent().visible_playlist_tab()._insert_track( self.session, track) # Select search text to make it easier for next search self.select_searchtext() def select_searchtext(self): + #TODO: V2 check self.ui.searchString.selectAll() self.ui.searchString.setFocus() def selection_changed(self): + #TODO: V2 check if not self.ui.matchList.selectedItems(): return item = self.ui.matchList.currentItem() track_id = item.data(Qt.UserRole) self.ui.dbPath.setText(Tracks.get_path(self.session, track_id)) + #TODO: V2 check class SelectPlaylistDialog(QDialog): def __init__(self, parent=None, playlist_dbs=None): + #TODO: V2 check super().__init__(parent) if playlist_dbs is None: @@ -932,6 +984,7 @@ class SelectPlaylistDialog(QDialog): self.ui.lstPlaylists.addItem(p) def __del__(self): + #TODO: V2 check with Session() as session: record = Settings.get_int(session, "select_playlist_dialog_height") if record.f_int != self.height(): @@ -942,14 +995,17 @@ class SelectPlaylistDialog(QDialog): record.update(session, {'f_int': self.width()}) def list_doubleclick(self, entry): + #TODO: V2 check self.plid = entry.data(Qt.UserRole) self.accept() def open(self): + #TODO: V2 check if self.ui.lstPlaylists.selectedItems(): item = self.ui.lstPlaylists.currentItem() self.plid = item.data(Qt.UserRole) self.accept() + #TODO: V2 check def main(): @@ -963,5 +1019,6 @@ def main(): EXCEPTION("Unhandled Exception caught by musicmuster.main()") +print(f"{__name__=}") if __name__ == "__main__": main() diff --git a/app/playlists.py b/app/playlists.py index 1181521..49df141 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -11,6 +11,7 @@ from PyQt5.QtWidgets import ( QTableWidget, QTableWidgetItem, ) +from sqlalchemy import inspect import helpers import os @@ -28,7 +29,7 @@ from models import ( Tracks, NoteColours ) -from utilities import create_track_from_file, update_meta +from utilities import create_track_from_file class PlaylistTab(QTableWidget): @@ -37,7 +38,7 @@ class PlaylistTab(QTableWidget): cellEditingEnded = QtCore.pyqtSignal() # Column names - COL_INDEX = 0 + COL_AUTOPLAY = COL_USERDATA = 0 COL_MSS = 1 COL_NOTE = 2 COL_TITLE = 2 @@ -45,19 +46,23 @@ class PlaylistTab(QTableWidget): COL_DURATION = 4 COL_START_TIME = 5 COL_END_TIME = 6 - COL_LAST_PLAYED = 7 - COL_LAST = 7 + COL_LAST_PLAYED = COL_LAST = 7 NOTE_COL_SPAN = COL_LAST - COL_NOTE + 1 NOTE_ROW_SPAN = 1 - def __init__(self, *args, **kwargs): + # Qt.UserRoles + ROW_METADATA = Qt.UserRole + CONTENT_OBJECT = Qt.UserRole + 1 + + def __init__(self, parent, session, playlist_db, *args, **kwargs): super().__init__(*args, **kwargs) - self.id = None - self.name = None - self.is_playlist = True - self.master_process = self.parent() + self.master_process = self.parent() # The MusicMuster process + self.playlist = playlist_db + self.playlist.mark_open(session) + + # Set up widget self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.setAlternatingRowColors(True) self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) @@ -65,6 +70,7 @@ class PlaylistTab(QTableWidget): self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) self.setRowCount(0) self.setColumnCount(8) + # Add header row item = QtWidgets.QTableWidgetItem() self.setHorizontalHeaderItem(0, item) item = QtWidgets.QTableWidgetItem() @@ -84,8 +90,16 @@ class PlaylistTab(QTableWidget): self.horizontalHeader().setMinimumSectionSize(0) self._set_column_widths() - self.setHorizontalHeaderLabels(["ID", "Lead", "Title", "Artist", - "Len", "Start", "End", "Last played"]) + self.setHorizontalHeaderLabels([ + Config.COLUMN_NAME_AUTOPLAY, + Config.COLUMN_NAME_LEADING_SILENCE, + Config.COLUMN_NAME_TITLE, + Config.COLUMN_NAME_ARTIST, + Config.COLUMN_NAME_LENGTH, + Config.COLUMN_NAME_START_TIME, + Config.COLUMN_NAME_END_TIME, + Config.COLUMN_NAME_LAST_PLAYED, + ]) self.setDragEnabled(True) self.setAcceptDrops(True) @@ -97,7 +111,7 @@ class PlaylistTab(QTableWidget): self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setDragDropMode(QAbstractItemView.InternalMove) - # This property holds how the widget shows a context menu + # This property defines how the widget shows a context menu self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) # This signal is emitted when the widget's contextMenuPolicy is # Qt::CustomContextMenu, and the user has requested a context @@ -113,12 +127,14 @@ class PlaylistTab(QTableWidget): self.cellEditingStarted.connect(self._cell_edit_started) self.cellEditingEnded.connect(self._cell_edit_ended) + self.populate(session) self.current_track_start_time = None self.played_tracks = [] # ########## Events ########## def dropEvent(self, event: QDropEvent): + # TODO: V2 check if not event.isAccepted() and event.source() == self: drop_row = self._drop_on(event) @@ -165,16 +181,19 @@ class PlaylistTab(QTableWidget): self.update_display() def edit(self, index, trigger, event): + # TODO: V2 check result = super(PlaylistTab, self).edit(index, trigger, event) if result: self.cellEditingStarted.emit(index.row(), index.column()) return result def closeEditor(self, editor, hint): + # TODO: V2 check super(PlaylistTab, self).closeEditor(editor, hint) self.cellEditingEnded.emit() def eventFilter(self, source, event): + # TODO: V2 check "Used to process context (right-click) menu" if(event.type() == QtCore.QEvent.MouseButtonPress and # noqa W504 @@ -215,6 +234,7 @@ class PlaylistTab(QTableWidget): # ########## Externally called functions ########## def close(self, session): + # TODO: V2 check "Save column widths" for column in range(self.columnCount()): @@ -224,136 +244,22 @@ class PlaylistTab(QTableWidget): if record.f_int != self.columnWidth(column): record.update(session, {'f_int': width}) - def insert_note(self, session, note, repaint=True): - """ - Add note to playlist - - If a row is selected, add note above. Otherwise, add to end of - playlist. - - Return the row number that track is now in. - """ - - if self.selectionModel().hasSelection(): - row = self.currentRow() - else: - row = self.rowCount() - DEBUG(f"playlist.inset_note(): row={row}") - - # Does note end with a time? - start_time = None - try: - start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time() - DEBUG( - f"playlist.inset_note(): Note contains valid time={start_time}" - ) - except ValueError: - DEBUG( - f"playlist.inset_note(): Note on row {row} ('{note.note}') " - "does not contain valid time" - ) - - self.insertRow(row) - - item = QTableWidgetItem(str(note.id)) - self.setItem(row, self.COL_INDEX, item) - titleitem = QTableWidgetItem(note.note) - self.setItem(row, self.COL_NOTE, titleitem) - self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, - self.NOTE_COL_SPAN) - - # Add start/end times or empty items as background - # colour won't be set for columns without items - self._set_row_start_time(row, start_time) - item = QTableWidgetItem() - self.setItem(row, self.COL_END_TIME, item) - item = QTableWidgetItem() - self.setItem(row, self.COL_LAST_PLAYED, item) - - self._meta_set_note(row) - - # Scroll to new row - self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) - - if repaint: - self.save_playlist(session) - self.update_display(clear_selection=False) - - return row - - def insert_track(self, session, track, repaint=True): - """ - Insert track into on-screen playlist. - - If a row is selected, add track above. Otherwise, add to end of - playlist. - - Return the row number that track is now in. - """ - - if self.selectionModel().hasSelection(): - row = self.currentRow() - else: - row = self.rowCount() - DEBUG( - f"playlists.insert_track({session=}, {track=}, {repaint=}), " - f"{row=}" - ) - - self.insertRow(row) - - item = QTableWidgetItem(str(track.id)) - self.setItem(row, self.COL_INDEX, item) - item = QTableWidgetItem(str(track.start_gap)) - if track.start_gap >= 500: - item.setBackground(QColor(Config.COLOUR_LONG_START)) - self.setItem(row, self.COL_MSS, item) - titleitem = QTableWidgetItem(track.title) - self.setItem(row, self.COL_TITLE, titleitem) - item = QTableWidgetItem(track.artist) - self.setItem(row, self.COL_ARTIST, item) - item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) - self.setItem(row, self.COL_DURATION, item) - last_playtime = Playdates.last_played(session, track.id) - last_played_str = get_relative_date(last_playtime) - item = QTableWidgetItem(last_played_str) - self.setItem(row, self.COL_LAST_PLAYED, item) - # Add empty start time for now as background - # colour won't be set for columns without items - item = QTableWidgetItem() - self.setItem(row, self.COL_START_TIME, item) - - # Scroll to new row - self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) - - if not self._track_path_is_readable(track.id): - self._meta_set_unreadable(row) - - if repaint: - self.save_playlist(session) - self.update_display(clear_selection=False) - - return row - def clear_current(self): + # TODO: V2 check "Clear current track" self._meta_clear_current() self.update_display() def clear_next(self): + # TODO: V2 check """Clear next track""" self._meta_clear_next() self.update_display() - def get_next_track_id(self): - "Return next track id" - - next_row = self._meta_get_next() - return self._get_row_id(next_row) - def get_selected_row(self): + # TODO: V2 check "Return row number of first selected row, or None if none selected" if not self.selectionModel().hasSelection(): @@ -362,6 +268,7 @@ class PlaylistTab(QTableWidget): return self.selectionModel().selectedRows()[0].row() def get_selected_rows_and_tracks(self): + # TODO: V2 check "Return a list of selected (rows, track_id) tuples" if not self.selectionModel().hasSelection(): @@ -376,6 +283,7 @@ class PlaylistTab(QTableWidget): return result def get_selected_title(self): + # TODO: V2 check "Return title of selected row or None" if self.selectionModel().hasSelection(): @@ -385,6 +293,7 @@ class PlaylistTab(QTableWidget): return None def remove_rows(self, rows): + # TODO: V2 check "Remove rows passed in rows list" # Row number will change as we delete rows. We could use @@ -400,6 +309,7 @@ class PlaylistTab(QTableWidget): self.update_display() def play_started(self): + # TODO: V2 check """ Update current track to be what was next, and determine next track. Return next track_id. @@ -428,32 +338,26 @@ class PlaylistTab(QTableWidget): return next_track_id def play_stopped(self): + # TODO: V2 check self._meta_clear_current() self.current_track_start_time = None self.update_display() - def populate(self, session, playlist_db): + def populate(self, session): """ - Populate ourself from the passed playlist_db object + Populate ourself from the associated playlist object + + We don't mandate that an item will be on its specified row, only + that it will be above larger-numbered row items, and below + lower-numbered ones. """ - # We don't mandate that an item will be - # on its specified row, only that it will be above - # larger-numbered row items, and below lower-numbered ones. - - # That means we need to re-save ourself once loaded to ensure - # database is correct. - - # First, save our id for the future - self.id = playlist_db.id - self.name = playlist_db.name - data = [] - for t in playlist_db.tracks: - data.append(([t.row], t.tracks)) - for n in playlist_db.notes: - data.append(([n.row], n)) + for row, track in self.playlist.tracks.items(): + data.append(([row], track)) + for note in self.playlist.notes: + data.append(([note.row], note)) # Clear playlist self.setRowCount(0) @@ -462,33 +366,59 @@ class PlaylistTab(QTableWidget): for i in sorted(data, key=lambda x: x[0]): item = i[1] if isinstance(item, Tracks): - self.insert_track(session, item, repaint=False) + self._insert_track(session, item, repaint=False) elif isinstance(item, Notes): - self.insert_note(session, item, repaint=False) + self._insert_note(session, item, repaint=False) # Scroll to top - scroll_to = self.item(0, self.COL_INDEX) + scroll_to = self.item(0, self.COL_TITLE) self.scrollToItem(scroll_to, QAbstractItemView.PositionAtTop) + # We possibly don't need to save the playlist here, but a) row + # numbers may have changed during population, and b) it's cheap self.save_playlist(session) self.update_display() def save_playlist(self, session): + # TODO: V2 check """ Save playlist to database. For notes: check the database entry is correct and update it if - necessary. Playlists:Note is one:many, so there is only one notes - appearance in all playlists. + necessary. Playlists:Note is one:many, so each note may only appear + in one playlist. For tracks: erase the playlist tracks and recreate. This is much - simpler than trying to correct any Playlists:Tracks many:many - errors. + simpler than trying to implement any Playlists:Tracks many:many + changes. """ - # We need to add ourself to the session - playlist_db = session.query(Playlists).filter( - Playlists.id == self.id).one() + # TODO: do we need to add ourself to the session? + insp = inspect(self.playlist) + transient = insp.transient + pending = insp.pending + persistent = insp.persistent + deleted = insp.deleted + detached = insp.detached + if transient: + DEBUG("playlist is transient") + session.add(self.playlist) + elif pending: + DEBUG("playlist is pending") + elif persistent: + DEBUG("playlist is persistent") + elif deleted: + DEBUG("playlist is deleted") + elif detached: + DEBUG("playlist is detached") + session.add(self.playlist) + assert inspect(self.playlist) == pending + else: + DEBUG("Can't find state of playlist") + + # TODO: hopefully we don't need to do this: + # playlist = session.query(Playlists).filter( + # Playlists.id == TODO: self.id).one() # Notes first # Create dictionaries indexed by note_id @@ -498,54 +428,46 @@ class PlaylistTab(QTableWidget): # PlaylistTab for row in notes_rows: - note_id = self._get_row_id(row) - if not note_id: - DEBUG(f"(_save_playlist(): no COL_INDEX data in row {row}") - continue - playlist_notes[note_id] = row + playlist_notes[note.id] = self._get_row_content(row) # Database - for note in playlist_db.notes: - database_notes[note.id] = note.row + for note in self.playlist.notes: + database_notes[note.id] = note - # Notes to add to database - # This should never be needed as notes are added to a specific - # playlist upon creation - for note_id in set(playlist_notes.keys()) - set(database_notes.keys()): - ERROR( - f"_save_playlist(): Note.id={note_id} " - f"missing from playlist {playlist_db} in database" - ) + # We don't need to check for notes to add to the database as + # they can't exist in the playlist without being in the database + # and pointing at this playlist. # Notes to remove from database for note_id in set(database_notes.keys()) - set(playlist_notes.keys()): DEBUG( - f"_save_playlist(): Delete note note_id={note_id} " - f"from playlist {playlist_db} in database" + "_save_playlist(): " + f"Delete {note_id=} from {playlist=} in database" ) - Notes.delete_note(session, note_id) + database_notes[note_id].delete_note(session) # Note rows to update in playlist database for note_id in set(playlist_notes.keys()) & set(database_notes.keys()): - if playlist_notes[note_id] != database_notes[note_id]: + if playlist_notes[note_id].row != database_notes[note_id].row: DEBUG( - f"_save_playlist(): Update database note.id {note_id} " - f"from row={database_notes[note_id]} to " - f"row={playlist_notes[note_id]}" + f"_save_playlist(): Update notes row in database " + f"from {database_notes[note_id]=} " + f"to {playlist_notes[note_id]=}" ) - Notes.update_note(session, note_id, playlist_notes[note_id]) + database_notes[note_id].update_note( + session, row=playlist_notes[note_id].row) # Tracks - # Remove all tracks for us in datbase - playlist_db.remove_all_tracks(session) + # Remove all tracks from this playlist + self.playlist.remove_all_tracks(session) # Iterate on-screen playlist and add tracks back in for row in range(self.rowCount()): if row in notes_rows: continue - playlist_db.add_track( - session, self.id, self._get_row_id(row), row) + playlist.add_track(session, track, row) def select_next_row(self): + # TODO: V2 check """ Select next or first row. Don't select notes. Wrap at last row. """ @@ -578,6 +500,7 @@ class PlaylistTab(QTableWidget): self.selectRow(row) def select_played_tracks(self): + # TODO: V2 check """Select all played tracks in playlist""" # Need to allow multiple rows to be selected @@ -593,6 +516,7 @@ class PlaylistTab(QTableWidget): self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) def select_previous_row(self): + # TODO: V2 check """ Select previous or last track. Don't select notes. Wrap at first row. """ @@ -626,6 +550,7 @@ class PlaylistTab(QTableWidget): self.selectRow(row) def select_unplayed_tracks(self): + # TODO: V2 check "Select all unplayed tracks in playlist" # Need to allow multiple rows to be selected @@ -645,6 +570,7 @@ class PlaylistTab(QTableWidget): self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) def set_selected_as_next(self): + # TODO: V2 check """ Sets the selected track as the next track. """ @@ -656,12 +582,10 @@ class PlaylistTab(QTableWidget): self.update_display() def update_display(self, clear_selection=True): + # TODO: V2 check "Set row colours, fonts, etc" - DEBUG( - f"playlist[{self.id}:{self.name}]." - f"_repaint(clear_selection={clear_selection}" - ) + DEBUG(f"playlist.update_display [{self.playlist=}]") with Session() as session: if clear_selection: @@ -694,6 +618,40 @@ class PlaylistTab(QTableWidget): # current track. if row in notes: + # TODO: check whether note has a time + + # # Does note end with a time? + # start_time = None + # try: + # start_time = datetime.strptime(note.note[-9:], " %H:%M:%S").time() + # DEBUG( + # f"playlist.inset_note(): Note contains valid time={start_time}" + # ) + # except ValueError: + # DEBUG( + # f"playlist.inset_note(): Note on row {row} ('{note.note}') " + # "does not contain valid time" + # ) + + # self.insertRow(row) + + # item = QTableWidgetItem(note) + # self.setItem(row, self.COL_INDEX, item) + # titleitem = QTableWidgetItem(note.note) + # self.setItem(row, self.COL_NOTE, titleitem) + # self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + # self.NOTE_COL_SPAN) + + # # Add start/end times or empty items as background + # # colour won't be set for columns without items + # self._set_row_start_time(row, start_time) + # item = QTableWidgetItem() + # self.setItem(row, self.COL_END_TIME, item) + # item = QTableWidgetItem() + # self.setItem(row, self.COL_LAST_PLAYED, item) + + # self._meta_set_note(row) + row_time = self._get_row_time(row) if row_time: next_start_time = row_time @@ -791,6 +749,7 @@ class PlaylistTab(QTableWidget): # ########## Internally called functions ########## def _audacity(self, row): + # TODO: V2 check "Open track in Audacity. Audacity must be already running" DEBUG(f"_audacity({row})") @@ -805,6 +764,7 @@ class PlaylistTab(QTableWidget): open_in_audacity(track.path) def _calculate_next_start_time(self, session, row, start): + # TODO: V2 check "Return this row's end time given its start time" if start is None: @@ -817,10 +777,12 @@ class PlaylistTab(QTableWidget): return start + timedelta(milliseconds=duration) def _context_menu(self, pos): + # TODO: V2 check self.menu.exec_(self.mapToGlobal(pos)) def _copy_path(self, row): + # TODO: V2 check """ If passed row is track row, copy the track path to the clipboard. Otherwise return None. @@ -840,6 +802,7 @@ class PlaylistTab(QTableWidget): cb.setText(path, mode=cb.Clipboard) def _cell_changed(self, row, column): + # TODO: V2 check "Called when cell content has changed" if not self.editing_cell: @@ -880,18 +843,20 @@ class PlaylistTab(QTableWidget): else: track = Tracks.get_by_id(session, row_id) if column == self.COL_ARTIST: - update_meta(session, track, artist=new) + track.update_artist(session, artist=new) elif column == self.COL_TITLE: - update_meta(session, track, title=new) + track.update_title(session, title=new) else: ERROR("_cell_changed(): unrecognised column") def _cell_edit_started(self, row, column): + # TODO: V2 check DEBUG(f"_cell_edit_started({row=}, {column=})") self.editing_cell = True self.master_process.disable_play_next_controls() def _cell_edit_ended(self): + # TODO: V2 check DEBUG("_cell_edit_ended()") self.editing_cell = False @@ -902,6 +867,7 @@ class PlaylistTab(QTableWidget): self.master_process.enable_play_next_controls() def _delete_rows(self): + # TODO: V2 check "Delete mutliple rows" DEBUG("playlist._delete_rows()") @@ -937,6 +903,7 @@ class PlaylistTab(QTableWidget): self.update_display() def _drop_on(self, event): + # TODO: V2 check index = self.indexAt(event.pos()) if not index.isValid(): return self.rowCount() @@ -944,7 +911,13 @@ class PlaylistTab(QTableWidget): return (index.row() + 1 if self._is_below(event.pos(), index) else index.row()) + def _get_row_content(self, row): + """Return content associated with this row""" + + return self.item(row, self.COL_USERDATA).data(CONTENT_OBJECT) + def _get_row_id(self, row): + # TODO: V2 check "Return item id as integer from passed row" if row is None: @@ -962,6 +935,7 @@ class PlaylistTab(QTableWidget): return None def _get_row_time(self, row): + # TODO: V2 check try: if self.item(row, self.COL_START_TIME): return datetime.strptime(self.item( @@ -973,6 +947,7 @@ class PlaylistTab(QTableWidget): return None def _info_row(self, row): + # TODO: V2 check "Display popup with info re row" id = self._get_row_id(row) @@ -1002,7 +977,111 @@ class PlaylistTab(QTableWidget): info.setDefaultButton(QMessageBox.Cancel) info.exec() + def _insert_note(self, session, note, repaint=True): + """ + Insert a note to playlist tab. + + If a row is selected, add note above. Otherwise, add to end of + playlist. + + Return the row number that track is now in. + """ + + if self.selectionModel().hasSelection(): + row = self.currentRow() + else: + row = self.rowCount() + DEBUG(f"playlist.inset_note(): row={row}") + + self.insertRow(row) + # Add empty items to unused columns because + # colour won't be set for columns without items + item = QTableWidgetItem() + self.setItem(row, self.COL_AUTOPLAY, item) + item = QTableWidgetItem() + self.setItem(row, self.COL_MSS, item) + # Add text of note from title column onwards + titleitem = QTableWidgetItem(note.note) + self.setItem(row, self.COL_NOTE, titleitem) + self.setSpan(row, self.COL_NOTE, self.NOTE_ROW_SPAN, + self.NOTE_COL_SPAN) + # Attach note object to row + self._set_row_content(row, note) + # Mark row as a Note row + self._meta_set_note(row) + + # Scroll to new row + self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) + + if repaint: + self.save_playlist(session) + self.update_display(clear_selection=False) + + return row + + def _insert_track(self, session, track, repaint=True): + """ + Insert track into playlist tab. + + If a row is selected, add track above. Otherwise, add to end of + playlist. + + Return the row number that track is now in. + """ + + if self.selectionModel().hasSelection(): + row = self.currentRow() + else: + row = self.rowCount() + DEBUG( + f"playlists.insert_track({session=}, {track=}, {repaint=}), " + f"{row=}" + ) + + self.insertRow(row) + # Add track details to columns + mss_item = QTableWidgetItem(str(track.start_gap)) + if track.start_gap >= 500: + item.setBackground(QColor(Config.COLOUR_LONG_START)) + self.setItem(row, self.COL_MSS, mss_item) + + title_item = QTableWidgetItem(track.title) + self.setItem(row, self.COL_TITLE, title_item) + + artist_item = QTableWidgetItem(track.artist) + self.setItem(row, self.COL_ARTIST, artist_item) + + duration_item = QTableWidgetItem(helpers.ms_to_mmss(track.duration)) + self.setItem(row, self.COL_DURATION, duration_item) + + last_playtime = Playdates.last_played(session, track.id) + last_played_str = get_relative_date(last_playtime) + last_played_item = QTableWidgetItem(last_played_str) + self.setItem(row, self.COL_LAST_PLAYED, last_played_item) + + # Add empty start and stop time because background + # colour won't be set for columns without items + start_item = QTableWidgetItem() + self.setItem(row, self.COL_START_TIME, start_item) + stop_item = QTableWidgetItem() + self.setItem(row, self.COL_STOP_TIME, stop_item) + # Attach track object to row + self._set_row_content(row, track) + + # Mart track if file is unreadable + if not os.access(track.path, os.R_OK): + self._meta_set_unreadable(row) + # Scroll to new row + self.scrollToItem(titleitem, QAbstractItemView.PositionAtCenter) + + if repaint: + self.save_playlist(session) + self.update_display(clear_selection=False) + + return row + def _is_below(self, pos, index): + # TODO: V2 check rect = self.visualRect(index) margin = 2 if pos.y() - rect.top() < margin: @@ -1016,6 +1095,7 @@ class PlaylistTab(QTableWidget): ) def _edit_cell(self, mi): + # TODO: V2 check "Called when table is double-clicked" row = mi.row() @@ -1026,6 +1106,7 @@ class PlaylistTab(QTableWidget): self.editItem(item) def _find_next_track_row(self, starting_row=None): + # TODO: V2 check """ Find next track to play. @@ -1054,11 +1135,13 @@ class PlaylistTab(QTableWidget): return None def _meta_clear(self, row): + # TODO: V2 check "Clear metadata for row" self._meta_set(row, None) def _meta_clear_current(self): + # TODO: V2 check """ Clear current row if there is one. There may not be if we've changed playlists @@ -1069,6 +1152,7 @@ class PlaylistTab(QTableWidget): self._meta_clear(current_row) def _meta_clear_next(self): + # TODO: V2 check """ Clear next row if there is one. There may not be if we've changed playlists @@ -1079,6 +1163,8 @@ class PlaylistTab(QTableWidget): self._meta_clear(next_row) def _meta_find(self, metadata, one=True): + + # TODO: V2 check """ Search rows for metadata. @@ -1108,31 +1194,37 @@ class PlaylistTab(QTableWidget): raise AttributeError(f"Multiple '{metadata}' metadata {matches}") def _meta_get(self, row): + # TODO: V2 check "Return row metadata" return self.item(row, self.COL_INDEX).data(Qt.UserRole) def _meta_get_current(self): + # TODO: V2 check "Return row marked as current, or None" return self._meta_find("current") def _meta_get_next(self): + # TODO: V2 check "Return row marked as next, or None" return self._meta_find("next") def _meta_get_notes(self): + # TODO: V2 check "Return rows marked as notes, or None" return self._meta_find("note", one=False) def _meta_get_unreadable(self): + # TODO: V2 check "Return rows marked as unreadable, or None" return self._meta_find("unreadable", one=False) def _meta_set_current(self, row): + # TODO: V2 check "Mark row as current track" old_current = self._meta_get_current() @@ -1141,6 +1233,7 @@ class PlaylistTab(QTableWidget): self._meta_set(row, "current") def _meta_set_next(self, row): + # TODO: V2 check "Mark row as next track" old_next = self._meta_get_next() @@ -1149,16 +1242,19 @@ class PlaylistTab(QTableWidget): self._meta_set(row, "next") def _meta_set_note(self, row): + # TODO: V2 check "Mark row as note" self._meta_set(row, "note") def _meta_set_unreadable(self, row): + # TODO: V2 check "Mark row as unreadable" self._meta_set(row, "unreadable") def _meta_set(self, row, metadata): + # TODO: V2 check "Set row metadata" if self.item(row, self.COL_TITLE): @@ -1166,7 +1262,7 @@ class PlaylistTab(QTableWidget): else: title = "" DEBUG( - f"playlist[{self.id}:{self.name}]._meta_set(row={row}, " + f"playlist[{TODO: self.id}:{TODO: self.name}]._meta_set(row={row}, " f"title={title}, metadata={metadata})" ) if row is None: @@ -1175,6 +1271,7 @@ class PlaylistTab(QTableWidget): self.item(row, self.COL_INDEX).setData(Qt.UserRole, metadata) def _set_next(self, row): + # TODO: V2 check """ If passed row is track row, check track is readable and, if it is, set that track as the next track to be played and return track_id. @@ -1201,6 +1298,7 @@ class PlaylistTab(QTableWidget): return track_id def _rescan(self, row): + # TODO: V2 check """ If passed row is track row, rescan it. Otherwise return None. @@ -1219,6 +1317,7 @@ class PlaylistTab(QTableWidget): self._update_row(row, track) def _select_event(self): + # TODO: V2 check """ Called when item selection changes. If multiple rows are selected, display sum of durations in status bar. @@ -1240,19 +1339,25 @@ class PlaylistTab(QTableWidget): self.master_process.lblSumPlaytime.setText("") def _set_column_widths(self): + # TODO: V2 check # Column widths from settings with Session() as session: for column in range(self.columnCount()): # Only show column 0 in test mode + # TODO: do we need column zero? Has no width ever. if (column == 0 and not Config.TESTMODE): self.setColumnWidth(0, 0) else: name = f"playlist_col_{str(column)}_width" record = Settings.get_int(session, name) - if record.f_int is not None: + if record and record.f_int is not None: self.setColumnWidth(column, record.f_int) + else: + self.setColumnWidth(column, + Config.DEFAULT_COLUMN_WIDTH) def _set_row_bold(self, row, bold=True): + # TODO: V2 check boldfont = QFont() boldfont.setBold(bold) for j in range(self.columnCount()): @@ -1260,14 +1365,22 @@ class PlaylistTab(QTableWidget): self.item(row, j).setFont(boldfont) def _set_row_colour(self, row, colour): + # TODO: V2 check for j in range(2, self.columnCount()): if self.item(row, j): self.item(row, j).setBackground(colour) + def _set_row_content(self, row, content): + """Set content associated with this row""" + + self.item(row, self.COL_USERDATA).setData(CONTENT_OBJECT, content) + def _set_row_not_bold(self, row): + # TODO: V2 check self._set_row_bold(row, False) def _set_row_end_time(self, row, time): + # TODO: V2 check "Set passed row end time to passed time" try: time_str = time.strftime("%H:%M:%S") @@ -1277,6 +1390,7 @@ class PlaylistTab(QTableWidget): self.setItem(row, self.COL_END_TIME, item) def _set_row_start_time(self, row, time): + # TODO: V2 check """Set passed row start time to passed time""" try: time_str = time.strftime("%H:%M:%S") @@ -1302,6 +1416,7 @@ class PlaylistTab(QTableWidget): return False def _update_row(self, row, track): + # TODO: V2 check """ Update the passed row with info from the passed track. """ diff --git a/app/utilities.py b/app/utilities.py index 2f631f6..3a2f5fc 100755 --- a/app/utilities.py +++ b/app/utilities.py @@ -5,10 +5,10 @@ import os import shutil import tempfile -from app.config import Config -from app.helpers import show_warning -from app.log import DEBUG, INFO -from app.models import Notes, Playdates, Session, Tracks +from config import Config +from helpers import show_warning +from log import DEBUG, INFO +from models import Notes, Playdates, Session, Tracks from mutagen.flac import FLAC from mutagen.mp3 import MP3 from pydub import AudioSegment, effects diff --git a/poetry.lock b/poetry.lock index d444059..2ffe774 100644 --- a/poetry.lock +++ b/poetry.lock @@ -374,6 +374,21 @@ tomli = ">=1.0.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "pytest-qt" +version = "4.0.2" +description = "pytest support for PyQt and PySide applications" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +pytest = ">=3.0.0" + +[package.extras] +dev = ["pre-commit", "tox"] +doc = ["sphinx", "sphinx-rtd-theme"] + [[package]] name = "python-vlc" version = "3.0.12118" @@ -829,6 +844,10 @@ pytest = [ {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, ] +pytest-qt = [ + {file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"}, + {file = "pytest_qt-4.0.2-py2.py3-none-any.whl", hash = "sha256:e03847ac02a890ccaac0fde1748855b9dce425aceba62005c6cfced6cf7d5456"}, +] python-vlc = [ {file = "python-vlc-3.0.12118.tar.gz", hash = "sha256:566f2f7c303f6800851cacc016df1c6eeec094ad63e0a49d87db9d698094f1fb"}, {file = "python_vlc-3.0.12118-py3-none-any.whl", hash = "sha256:f88be06c6f819a4db2de1c586b193b5df1410ff10fca33b8c6f4e56037c46f7b"}, diff --git a/pyproject.toml b/pyproject.toml index f8f73bf..05c2c45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ mypy = "^0.931" pytest = "^7.0.0" ipdb = "^0.13.9" sqlalchemy-stubs = "^0.4" +pytest-qt = "^4.0.2" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/test_models.py b/test_models.py index e33b32e..fcca199 100644 --- a/test_models.py +++ b/test_models.py @@ -46,7 +46,6 @@ def test_notecolours_get_colour_none(session): def test_notecolours_get_colour_match(session): - note_colour = "#abcdef" nc = NoteColours(session, substring="sub", colour=note_colour) assert nc @@ -146,7 +145,6 @@ def test_playdates_remove_track(session): def test_playlist_create(session): - playlist = Playlists(session, "my playlist") assert playlist @@ -164,7 +162,6 @@ def test_playlist_add_note(session): def test_playlist_add_track(session): - # We need a playlist playlist = Playlists(session, "my playlist") @@ -202,8 +199,27 @@ def test_playlist_tracks(session): assert tracks[track2_row] == track2 -def test_playlist_open_and_close(session): +def test_playlist_notes(session): + # We need a playlist + playlist = Playlists(session, "my playlist") + # We need two notes + note1_text = "note1 text" + note1_row = 11 + note1 = Notes(session, playlist.id, note1_row, note1_text) + + note2_text = "note2 text" + note2_row = 19 + note2 = Notes(session, playlist.id, note2_row, note2_text) + + notes = playlist.notes + assert note1_text in [n.note for n in notes] + assert note1_row in [n.row for n in notes] + assert note2_text in [n.note for n in notes] + assert note2_row in [n.row for n in notes] + + +def test_playlist_open_and_close(session): # We need a playlist playlist = Playlists(session, "my playlist") diff --git a/test_playlists.py b/test_playlists.py index 2b3a264..279f8a4 100644 --- a/test_playlists.py +++ b/test_playlists.py @@ -1,8 +1,10 @@ from app.playlists import PlaylistTab +from app.models import Playlists -def test_init(session): - """Just check we can create a playlist""" +def test_init(qtbot, session): + """Just check we can create a playlist_tab""" - playlist = PlaylistTab() - assert playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + assert playlist_tab