diff --git a/app/config.py b/app/config.py index 55bd173..0b1ab1e 100644 --- a/app/config.py +++ b/app/config.py @@ -39,6 +39,7 @@ class Config(object): FADE_STEPS = 20 FADE_TIME = 3000 INFO_TAB_TITLE_LENGTH = 15 + INFO_TAB_URL = "https://www.wikipedia.org/w/index.php?search=%s" LOG_LEVEL_STDERR = logging.INFO LOG_LEVEL_SYSLOG = logging.DEBUG LOG_NAME = "musicmuster" diff --git a/app/helpers.py b/app/helpers.py index d53535e..53fc81f 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -13,9 +13,9 @@ 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) + button_reply = QMessageBox.question(None, title, question) + + return button_reply == QMessageBox.Yes def fade_point(audio_segment, fade_threshold=0, diff --git a/app/models.py b/app/models.py index e316f66..ed3f416 100644 --- a/app/models.py +++ b/app/models.py @@ -19,7 +19,12 @@ from sqlalchemy import ( func ) from sqlalchemy.exc import IntegrityError -from sqlalchemy.orm import backref, relationship, sessionmaker, scoped_session +from sqlalchemy.orm import ( + backref, + relationship, + sessionmaker, + scoped_session +) from sqlalchemy.orm.collections import attribute_mapped_collection from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound @@ -129,7 +134,8 @@ class Notes(Base): id = Column(Integer, primary_key=True, autoincrement=True) playlist_id = Column(Integer, ForeignKey('playlists.id')) - playlist = relationship("Playlists", back_populates="notes") + playlist = relationship("Playlists", back_populates="notes", + lazy="joined") row = Column(Integer, nullable=False) note = Column(String(256), index=False) @@ -176,7 +182,8 @@ class Playdates(Base): id = Column(Integer, primary_key=True, autoincrement=True) lastplayed = Column(DateTime, index=True, default=None) track_id = Column(Integer, ForeignKey('tracks.id')) - tracks = relationship("Tracks", back_populates="playdates") + tracks = relationship("Tracks", back_populates="playdates", + lazy="joined") def __init__(self, session, track): """Record that track was played""" @@ -226,7 +233,8 @@ class Playlists(Base): loaded = Column(Boolean, default=True, nullable=False) notes = relationship("Notes", order_by="Notes.row", - back_populates="playlist") + back_populates="playlist", + lazy="joined") tracks = association_proxy('playlist_tracks', 'tracks') row = association_proxy('playlist_tracks', 'row') @@ -338,7 +346,8 @@ class PlaylistTracks(Base): Playlists, backref=backref( "playlist_tracks", - collection_class=attribute_mapped_collection("row") + collection_class=attribute_mapped_collection("row"), + lazy="joined" ) ) @@ -351,6 +360,39 @@ class PlaylistTracks(Base): session.add(self) session.commit() + @staticmethod + def move_track(session, from_playlist_id, row, to_playlist_id): + """ + Move track between playlists. This would be more efficient with + an ORM-enabled UPDATE statement, but this works just fine. + """ + DEBUG( + "PlaylistTracks.move_tracks(" + f"{from_playlist_id=}, {rows=}, {to_playlist_id=})" + ) + max_row = session.query(func.max(PlaylistTracks.row)).filter( + PlaylistTracks.playlist_id == to_playlist_id).scalar() + if max_row is None: + # Destination playlist is empty; use row 0 + new_row = 0 + else: + # Destination playlist has tracks; add to end + new_row = max_row + 1 + try: + record = session.query(PlaylistTracks).filter( + PlaylistTracks.playlist_id == from_playlist_id, + PlaylistTracks.row == row).one() + except NoResultFound: + ERROR( + f"No rows matched in query: " + f"PlaylistTracks.playlist_id == {from_playlist_id}, " + f"PlaylistTracks.row == {row}" + ) + return + record.playlist_id = to_playlist_id + record.row = new_row + session.commit() + @staticmethod def next_free_row(session, playlist): """Return next free row number""" @@ -409,8 +451,10 @@ class Tracks(Base): path = Column(String(2048), index=False, nullable=False) mtime = Column(Float, index=True) lastplayed = Column(DateTime, index=True, default=None) - playlists = relationship("PlaylistTracks", back_populates="tracks") - playdates = relationship("Playdates", back_populates="tracks") + playlists = relationship("PlaylistTracks", back_populates="tracks", + lazy="joined") + playdates = relationship("Playdates", back_populates="tracks", + lazy="joined") def __init__(self, session, path): self.path = path diff --git a/app/musicmuster.py b/app/musicmuster.py index 10a2f39..430f0f5 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -11,7 +11,7 @@ from datetime import datetime, timedelta from log import DEBUG, EXCEPTION from PyQt5.QtCore import QProcess, Qt, QTimer, QUrl -from PyQt5.QtGui import QColor, QFontMetrics, QPainter +from PyQt5.QtGui import QColor from PyQt5.QtWebEngineWidgets import QWebEngineView as QWebView from PyQt5.QtWidgets import ( QApplication, @@ -27,7 +27,7 @@ import helpers import music from config import Config -from models import (db_init, Notes, Playdates, Playlists, PlaylistTracks, +from models import (db_init, Playdates, Playlists, PlaylistTracks, Session, Settings, Tracks) from playlists import PlaylistTab from utilities import create_track_from_file @@ -38,7 +38,6 @@ from ui.main_window_ui import Ui_MainWindow class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None): - #TODO: V2 check super().__init__(parent) self.setupUi(self) @@ -58,7 +57,6 @@ class Window(QMainWindow, Ui_MainWindow): self.previous_track_position = None self.spnVolume.setValue(Config.VOLUME_VLC_DEFAULT) - self.menuTest.menuAction().setVisible(Config.TESTMODE) self.set_main_window_size() self.lblSumPlaytime = QLabel("") self.statusbar.addPermanentWidget(self.lblSumPlaytime) @@ -71,7 +69,7 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.start(Config.TIMER_MS) def add_file(self): - #TODO: V2 check + # TODO: V2 enahancement to import tracks dlg = QFileDialog() dlg.setFileMode(QFileDialog.ExistingFiles) dlg.setViewMode(QFileDialog.Detail) @@ -85,11 +83,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" + """Set size of window from database""" with Session() as session: record = Settings.get_int(session, "mainwindow_x") @@ -102,7 +99,8 @@ class Window(QMainWindow, Ui_MainWindow): height = record.f_int or 981 self.setGeometry(x, y, width, height) - def check_audacity(self): + @staticmethod + def check_audacity(): """Offer to run Audacity if not running""" if not Config.CHECK_AUDACITY_AT_STARTUP: @@ -115,13 +113,11 @@ class Window(QMainWindow, Ui_MainWindow): 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" + """Don't allow window to close when a track is playing""" if self.music.playing(): DEBUG("closeEvent() ignored as music is playing") @@ -159,9 +155,8 @@ 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.actionAdd_note.triggered.connect(self.create_note) self.action_Clear_selection.triggered.connect(self.clear_selection) self.actionClosePlaylist.triggered.connect(self.close_playlist_tab) self.actionExport_playlist.triggered.connect(self.export_playlist_tab) @@ -177,18 +172,16 @@ class Window(QMainWindow, Ui_MainWindow): self.select_previous_row) self.actionSelect_unplayed_tracks.triggered.connect( self.select_unplayed) - self.actionSetNext.triggered.connect(self.set_next_track) + self.actionSetNext.triggered.connect( + lambda: self.tabPlaylist.currentWidget().set_selected_as_next()) self.actionSkip_next.triggered.connect(self.play_next) - self.actionSkipToEnd.triggered.connect(self.test_skip_to_end) - self.actionSkipToFade.triggered.connect(self.test_skip_to_fade) self.actionStop.triggered.connect(self.stop) - self.actionTestFunction.triggered.connect(self.test_function) self.btnAddFile.clicked.connect(self.add_file) - self.btnAddNote.clicked.connect(self.insert_note) + self.btnAddNote.clicked.connect(self.create_note) self.btnDatabase.clicked.connect(self.search_database) self.btnFade.clicked.connect(self.fade) self.btnPlay.clicked.connect(self.play_next) - self.btnSetNext.clicked.connect(self.set_next_track) + self.btnSetNext.clicked.connect(self.this_is_the_next_track) self.btnSongInfo.clicked.connect(self.song_info_search) self.btnStop.clicked.connect(self.stop) self.spnVolume.valueChanged.connect(self.change_volume) @@ -197,8 +190,7 @@ class Window(QMainWindow, Ui_MainWindow): self.timer.timeout.connect(self.tick) def create_playlist(self): - #TODO: V2 check - "Create new playlist" + """Create new playlist""" dlg = QInputDialog(self) dlg.setInputMode(QInputDialog.TextInput) @@ -207,24 +199,21 @@ class Window(QMainWindow, Ui_MainWindow): ok = dlg.exec() if ok: with Session() as session: - playlist_db = Playlists(session, dlg.textValue()) - self.create_playlist_tab(session, playlist_db) + playlist = Playlists(session, dlg.textValue()) + self.create_playlist_tab(session, playlist) def change_volume(self, volume): - #TODO: V2 check - "Change player maximum volume" + """Change player maximum volume""" DEBUG(f"change_volume({volume})") 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 hasattr(self.tabPlaylist.widget(index), 'playlist'): if self.tabPlaylist.widget(index) == ( self.current_track_playlist_tab): self.statusbar.showMessage( @@ -236,49 +225,32 @@ class Window(QMainWindow, Ui_MainWindow): return # It's OK to close this playlist so remove from open playlist list with Session() as session: - playlist_db = session.query(Playlists).filter( - Playlists.id == self.tabPlaylist.widget(index).id).one() - playlist_db.close(session) + self.tabPlaylist.widget(index).playlist.close(session) + # Close regardless of tab type self.tabPlaylist.removeTab(index) - def create_note(self, session, text): - #TODO: V2 check - """ - Create note + def create_note(self): + """Call playlist to create note""" - If a row is selected, set note row to be rows above. Otherwise, - set note row to be end of playlist. - - Return note. - """ - - row = self.visible_playlist_tab().get_selected_row() - if row is None: - row = self.visible_playlist_tab().rowCount() - DEBUG(f"musicmuster.create_note(text={text}): row={row}") - - note = Notes.add_note( - session, self.visible_playlist_tab().id, row, text) - # TODO: this needs to call playlist.add_note now - - return note + try: + self.visible_playlist_tab().create_note() + except AttributeError: + # Just return if there's no visible playlist tab + return 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" + """Clean up after track played""" # Set self.playing to False so that tick() doesn't see # player=None and kick off end-of-track actions @@ -287,11 +259,10 @@ class Window(QMainWindow, Ui_MainWindow): # Clean up metadata if self.current_track: self.previous_track = self.current_track + self.current_track = None if self.current_track_playlist_tab: self.current_track_playlist_tab.play_stopped() - self.current_track_playlist_tab.clear_current() self.current_track_playlist_tab = None - self.current_track = None # Clean up display self.frame_fade.setStyleSheet("") @@ -303,10 +274,132 @@ class Window(QMainWindow, Ui_MainWindow): # Enable controls self.enable_play_next_controls() - def ensure_info_tabs(self, title_list): + def export_playlist_tab(self): + """Export the current playlist to an m3u file""" + + if not self.visible_playlist_tab(): + return + + # Get output filename + pathspec = QFileDialog.getSaveFileName( + self, 'Save Playlist', + directory=f"{self.visible_playlist_tab().name}.m3u", + filter="M3U files (*.m3u);;All files (*.*)" + ) + if not pathspec: + return + + path = pathspec[0] + if not path.endswith(".m3u"): + path += ".m3u" + + with open(path, "w") as f: + # Required directive on first line + f.write("#EXTM3U\n") + for track in self.playlist.tracks: + f.write( + "#EXTINF:" + f"{int(track.duration / 1000)}," + f"{track.title} - " + f"{track.artist}" + "\n" + f"{track.path}" + "\n" + ) + + def fade(self): + """Fade currently playing track""" + + DEBUG("musicmuster:fade()", True) + + if not self.current_track: + return + + self.previous_track_position = self.music.get_position() + self.music.fade() + self.end_of_track_actions() + + def insert_note(self): #TODO: V2 check + "Add non-track row to playlist" + + dlg = QInputDialog(self) + dlg.setInputMode(QInputDialog.TextInput) + dlg.setLabelText("Note:") + dlg.resize(500, 100) + ok = dlg.exec() + if ok: + with Session() as session: + note = self.create_note(session, dlg.textValue()) + self.visible_playlist_tab()._insert_note(session, note) + + def load_last_playlists(self): + """Load the playlists that we loaded at end of last session""" + + with Session() as session: + for playlist in Playlists.get_open(session): + self.create_playlist_tab(session, playlist) + + def create_playlist_tab(self, session, playlist): + """ + Take the passed playlist database object, create a playlist tab and + add tab to display. + """ + + playlist_tab = PlaylistTab(parent=self, + session=session, playlist=playlist) + idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) + self.tabPlaylist.setCurrentIndex(idx) + + def move_selected(self): + """Move selected rows to another playlist""" + + with Session() as session: + playlists = [p for p in Playlists.get_all(session) + if p.id != self.visible_playlist_tab().id] + dlg = SelectPlaylistDialog(self, playlists=playlists) + dlg.exec() + if not dlg.plid: + return + + # If destination playlist is visible, we need to add the moved + # tracks to it. If not, they will be automatically loaded when + # the playlistis opened. + destination_visible_playlist_tab = None + for tab in range(self.tabPlaylist.count()): + # Non-playlist tabs won't have a 'playlist' attribute + if not hasattr(self.tabPlaylist.widget(tab), 'playlist'): + continue + if self.tabPlaylist.widget(tab).id == dlg.plid: + destination_visible_playlist_tab = ( + self.tabPlaylist.widget(tab)) + break + + rows = [] + for (row, track) in ( + self.visible_playlist_tab().get_selected_rows_and_tracks() + ): + rows.append(row) + if destination_visible_playlist_tab: + # Insert with repaint=False to not update database + destination_visible_playlist_tab.insert_track( + session, track, repaint=False) + + # Update database for both source and destination playlists + PlaylistTracks.move_rows( + session, self.visible_playlist_tab().id, rows, dlg.plid) + + # Update destination playlist if visible + if destination_visible_playlist_tab: + destination_visible_playlist_tab.update_display() + + # Update source playlist + self.visible_playlist_tab().remove_rows(rows) + + def open_info_tab(self, title_list): """ Ensure we have info tabs for each of the passed titles + Called from update_headers """ for title in title_list: @@ -332,148 +425,15 @@ class Window(QMainWindow, Ui_MainWindow): idx, title[:Config.INFO_TAB_TITLE_LENGTH]) del self.info_tabs[old_title] else: + # Create a new tab for this title widget = self.info_tabs[title] = QWebView() self.tabPlaylist.addTab( widget, title[:Config.INFO_TAB_TITLE_LENGTH]) - str = urllib.parse.quote_plus(title) - url = f"https://www.wikipedia.org/w/index.php?search={str}" + txt = urllib.parse.quote_plus(title) + url = Config.INFO_TAB_URL % txt 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(): - return - - # Get output filename - pathspec = QFileDialog.getSaveFileName( - self, 'Save Playlist', - directory=f"{self.visible_playlist_tab().name}.m3u", - filter="M3U files (*.m3u);;All files (*.*)" - ) - if not pathspec: - return - - path = pathspec[0] - - if not path.endswith(".m3u"): - path += ".m3u" - - # Get playlist db object - with Session() as session: - playlist_db = Playlists.get_by_id( - session, self.visible_playlist_tab().id) - with open(path, "w") as f: - # Required directive on first line - f.write("#EXTM3U\n") - for track in playlist_db.tracks: - f.write( - "#EXTINF:" - f"{int(track.duration / 1000)}," - f"{track.title} - " - f"{track.artist}" - "\n" - f"{track.path}" - "\n" - ) - - def fade(self): - #TODO: V2 check - "Fade currently playing track" - - DEBUG("musicmuster:fade()", True) - - if not self.current_track: - return - - self.previous_track_position = self.music.fade() - self.end_of_track_actions() - - def insert_note(self): - #TODO: V2 check - "Add non-track row to playlist" - - dlg = QInputDialog(self) - dlg.setInputMode(QInputDialog.TextInput) - dlg.setLabelText("Note:") - dlg.resize(500, 100) - ok = dlg.exec() - if ok: - with Session() as session: - note = self.create_note(session, dlg.textValue()) - 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.create_playlist_tab(session, playlist_db) - - def create_playlist_tab(self, session, playlist): - #TODO: V2 check - """ - Take the passed playlist database object, create a playlist tab and - add tab to display. - """ - - playlist_tab = PlaylistTab(parent=self, - session=session, playlist=playlist) - idx = self.tabPlaylist.addTab(playlist_tab, playlist.name) - self.tabPlaylist.setCurrentIndex(idx) - - def move_selected(self): - #TODO: V2 check - "Move selected rows to another playlist" - - # TODO needs refactoring - - with Session() as session: - playlist_dbs = [p for p in Playlists.get_all(session) - if p.id != self.visible_playlist_tab().id] - dlg = SelectPlaylistDialog(self, playlist_dbs=playlist_dbs) - dlg.exec() - if not dlg.plid: - return - - # If destination playlist is visible, we need to add the moved - # tracks to it. If not, they will be automatically loaded when - # the playlistis opened. - destination_visible_playlist_tab = None - for tab in range(self.tabPlaylist.count()): - # Non-playlist tabs won't have ids - if not hasattr(self.tabPlaylist.widget(tab), 'id'): - continue - if self.tabPlaylist.widget(tab).id == dlg.plid: - destination_visible_playlist_tab = ( - self.tabPlaylist.widget(tab)) - break - - rows = [] - for (row, track) in ( - self.visible_playlist_tab().get_selected_rows_and_tracks() - ): - rows.append(row) - if destination_visible_playlist_tab: - # Insert with repaint=False to not update database - destination_visible_playlist_tab.insert_track( - session, track, repaint=False) - - # Update database for both source and destination playlists - PlaylistTracks.move_track( - session, self.visible_playlist_tab().id, row, dlg.plid) - - # Update destination playlist if visible - if destination_visible_playlist_tab: - destination_visible_playlist_tab.update_display() - - # Update source playlist - self.visible_playlist_tab().remove_rows(rows) - def play_next(self): - #TODO: V2 check """ Play next track. @@ -481,10 +441,10 @@ class Window(QMainWindow, Ui_MainWindow): If there's currently a track playing, fade it. Move next track to current track. Play (new) current. - Update playlist "current track" metadata + Tell playlist to update "current track" metadata. This will also + trigger a call to 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 """ @@ -507,36 +467,13 @@ class Window(QMainWindow, Ui_MainWindow): # Play next track self.current_track = self.next_track + self.next_track = None self.current_track_playlist_tab = self.next_track_playlist_tab + self.next_track_playlist_tab = None self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_CURRENT_TAB)) - self.next_track = None - self.next_track_playlist_tab = None - DEBUG( - "musicmuster.play_next: calling music.play(" - f"{self.current_track.path=})" - ) self.music.play(self.current_track.path) - # Update metadata - # Get next track for this playlist. May be None if there is - # no automatic next track, and may later be overriden by - # user selecting a different track on this or another - # playlist. - ***KAE won't return next_track_id *** = self.current_track_playlist_tab.play_started() - - if next_track_id is not None: - self.next_track = Tracks.get_by_id(session, next_track_id) - self.next_track_playlist_tab = self.current_track_playlist_tab - else: - self.next_track = self.next_track_playlist_tab = None - - if self.next_track_playlist_tab and ( - self.current_track_playlist_tab != - self.next_track_playlist_tab): - self.set_tab_colour(self.next_track_playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) - # Tell database to record it as played Playdates(session, self.current_track) @@ -554,83 +491,40 @@ 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) + playlists = Playlists.get_closed(session) + dlg = SelectPlaylistDialog(self, playlists=playlists) dlg.exec() if dlg.plid: - playlist_db = Playlists.get_by_id(session, dlg.plid) - self.create_playlist_tab(session, playlist_db) + playlist = Playlists.get_by_id(session, dlg.plid) + self.create_playlist_tab(session, playlist) def select_next_row(self): - #TODO: V2 check - "Select next or first row in playlist" + """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" + """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" + """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: - if not next_track_id: - next_track_id = ( - self.visible_playlist_tab().set_selected_as_next()) - if not next_track_id: - return - - # The next track has been selected on the currently-visible - # playlist. However, there may already be a 'next track' - # selected on another playlist that the user is overriding, - # in which case we need to reset that playlist. - if self.next_track_playlist_tab != self.visible_playlist_tab(): - # We need to reset the ex-next-track playlist - if self.next_track_playlist_tab: - self.next_track_playlist_tab.clear_next() - # Reset tab colour if it NOT the current playing tab - if (self.next_track_playlist_tab != - self.current_track_playlist_tab): - self.set_tab_colour(self.next_track_playlist_tab, - QColor(Config.COLOUR_NORMAL_TAB)) - self.next_track_playlist_tab = self.visible_playlist_tab() - # self.next_track_playlist_tab is now set to correct - # playlist - if (self.next_track_playlist_tab != - self.current_track_playlist_tab): - self.set_tab_colour(self.next_track_playlist_tab, - QColor(Config.COLOUR_NEXT_TAB)) - - self.next_track = Tracks.get_by_id(session, next_track_id) - - self.update_headers() - def select_unplayed(self): - #TODO: V2 check - "Select all unplayed tracks in playlist" + """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 """ @@ -639,9 +533,8 @@ 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 + Open browser tab for Wikipedia, searching for the first that exists of: - selected track - next track @@ -657,24 +550,23 @@ class Window(QMainWindow, Ui_MainWindow): title = self.current_track.title if title: # Wikipedia - str = urllib.parse.quote_plus(title) - url = f"https://www.wikipedia.org/w/index.php?search={str}" + txt = urllib.parse.quote_plus(title) + url = Config.INFO_TAB_URL % txt webbrowser.open(url, new=2) def stop(self): - #TODO: V2 check - "Stop playing immediately" + """Stop playing immediately""" DEBUG("musicmuster.stop()") self.stop_playing(fade=False) def stop_playing(self, fade=True): - #TODO: V2 check - "Stop playing current track" + """Stop playing current track""" DEBUG(f"musicmuster.stop_playing({fade=})", True) + # Set tab colour if self.current_track_playlist_tab == self.next_track_playlist_tab: self.set_tab_colour(self.current_track_playlist_tab, QColor(Config.COLOUR_NEXT_TAB)) @@ -701,32 +593,41 @@ class Window(QMainWindow, Ui_MainWindow): self.music.stop() self.update_headers() - def test_function(self): - #TODO: V2 check - "Placeholder for test function" + def this_is_the_next_track(self, playlist_tab, track): + """ + This is notification from a playlist tab that it holds the next + track to be played. + """ - pass + # The next track has been selected on the playlist_tab + # playlist. However, there may already be a 'next track' + # selected on another playlist that the user is overriding, + # in which case we need to reset that playlist. + if self.next_track_playlist_tab != playlist_tab: + # We need to reset the ex-next-track playlist + if self.next_track_playlist_tab: + self.next_track_playlist_tab.clear_next() + # Reset tab colour if it NOT the current playing tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NORMAL_TAB)) + self.next_track_playlist_tab = playlist_tab + # self.next_track_playlist_tab is now set to correct playlist + # Set the colour of the next playlist tab if it isn't the + # currently-playing tab + if (self.next_track_playlist_tab != + self.current_track_playlist_tab): + self.set_tab_colour( + self.next_track_playlist_tab, + QColor(Config.COLOUR_NEXT_TAB)) - def test_skip_to_end(self): - #TODO: V2 check - "Skip current track to 1 second before silence" + self.next_track = track - if not self.playing: - return - - 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(): - return - - self.music.set_position(self.current_track.fade_at - 1000) + self.update_headers() def tick(self): - #TODO: V2 check """ Update screen @@ -799,7 +700,6 @@ 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. @@ -831,12 +731,13 @@ class Window(QMainWindow, Ui_MainWindow): except AttributeError: self.hdrNextTrack.setText("") - self.ensure_info_tabs(titles) + self.open_info_tab(titles) class DbDialog(QDialog): + """Select track from database""" + def __init__(self, parent, session): - #TODO: V2 check super().__init__(parent) self.session = session self.ui = Ui_Dialog() @@ -846,7 +747,7 @@ class DbDialog(QDialog): self.ui.btnClose.clicked.connect(self.close) self.ui.matchList.itemDoubleClicked.connect(self.double_click) self.ui.matchList.itemSelectionChanged.connect(self.selection_changed) - self.ui.radioTitle.toggled.connect(self.radio_toggle) + self.ui.radioTitle.toggled.connect(self.title_artist_toggle) self.ui.searchString.textEdited.connect(self.chars_typed) record = Settings.get_int(self.session, "dbdialog_width") @@ -856,7 +757,6 @@ 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()}) @@ -866,21 +766,18 @@ 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 item = self.ui.matchList.currentItem() - track_id = item.data(Qt.UserRole) - self.add_track(track_id) + track = item.data(Qt.UserRole) + self.add_track(track) def add_selected_and_close(self): - #TODO: V2 check self.add_selected() self.close() - def radio_toggle(self): - #TODO: V2 check + def title_artist_toggle(self): """ Handle switching between searching for artists and searching for titles @@ -890,7 +787,6 @@ 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) @@ -904,49 +800,40 @@ class DbDialog(QDialog): f"{track.title} - {track.artist} " f"[{helpers.ms_to_mmss(track.duration)}]" ) - t.setData(Qt.UserRole, track.id) + t.setData(Qt.UserRole, track) self.ui.matchList.addItem(t) def double_click(self, entry): - #TODO: V2 check - track_id = entry.data(Qt.UserRole) - self.add_track(track_id) + track = entry.data(Qt.UserRole) + self.add_track(track) # 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) + def add_track(self, track): # 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 + track = item.data(Qt.UserRole) + self.ui.dbPath.setText(track.path) class SelectPlaylistDialog(QDialog): - def __init__(self, parent=None, playlist_dbs=None): - #TODO: V2 check + def __init__(self, parent=None, playlists=None): super().__init__(parent) - if playlist_dbs is None: + if playlists is None: return self.ui = Ui_dlgSelectPlaylist() self.ui.setupUi(self) @@ -962,14 +849,13 @@ class SelectPlaylistDialog(QDialog): height = record.f_int or 600 self.resize(width, height) - for (plid, plname) in [(a.id, a.name) for a in playlist_dbs]: + for (plid, plname) in [(a.id, a.name) for a in playlists]: p = QListWidgetItem() p.setText(plname) p.setData(Qt.UserRole, plid) 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(): @@ -980,17 +866,14 @@ 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(): diff --git a/app/playlists.py b/app/playlists.py index 5395b07..7f0eb55 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -11,7 +11,7 @@ from PyQt5.QtWidgets import ( QMenu, QMessageBox, QTableWidget, - QTableWidgetItem, + QTableWidgetItem, QInputDialog, ) from sqlalchemy import inspect @@ -136,11 +136,14 @@ class PlaylistTab(QTableWidget): self.cellEditingEnded.connect(self._cell_edit_ended) # Now load our tracks and notes - self.populate(session) + self._populate(session) self.current_track_start_time = None def __repr__(self): - return f"" + return ( + f"" + ) # ########## Events ########## @@ -189,8 +192,8 @@ class PlaylistTab(QTableWidget): self.save_playlist(session) self.update_display() - def edit(self, index): - result = super(PlaylistTab, self).edit(index) + def edit(self, index, trigger, event): + result = super(PlaylistTab, self).edit(index, trigger, event) if result: self.cellEditingStarted.emit(index.row(), index.column()) return result @@ -252,18 +255,35 @@ class PlaylistTab(QTableWidget): event.accept() - def clear_current(self): - """Clear current track""" - - self._meta_clear_current() - self.update_display() - def clear_next(self): """Clear next track""" self._meta_clear_next() self.update_display() + def create_note(self): + """ + Create note + + If a row is selected, set note row to be rows above. Otherwise, + set note row to be end of playlist. + """ + + row = self.get_selected_row() + if not row: + row = self.rowCount() + + # Get note text + dlg = QInputDialog(self) + dlg.setInputMode(QInputDialog.TextInput) + dlg.setLabelText("Note:") + dlg.resize(500, 100) + ok = dlg.exec() + if ok: + with Session() as session: + note = Notes(session, self.playlist.id, row, dlg.textValue()) + self._insert_note(session, note, row, True) + def get_selected_row(self): """Return row number of first selected row, or None if none selected""" @@ -294,6 +314,70 @@ class PlaylistTab(QTableWidget): else: return None + 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) + # Put an item in COL_USERDATA for later + item = QTableWidgetItem() + self.setItem(row, self.COL_USERDATA, item) + # Add track details to columns + mss_item = QTableWidgetItem(str(track.start_gap)) + if track.start_gap and 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_END_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(title_item, QAbstractItemView.PositionAtCenter) + + if repaint: + self.save_playlist(session) + self.update_display(clear_selection=False) + + return row + def remove_rows(self, rows): """Remove rows passed in rows list""" @@ -337,48 +421,6 @@ class PlaylistTab(QTableWidget): self.current_track_start_time = None self.update_display() - def populate(self, session): - """ - Populate 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. - """ - - data = [] - - # Make sure the database object is usable - insp = inspect(self.playlist) - if insp.detached: - session.add(self.playlist) - assert insp.persistent - - 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) - - # Now add data in row order - for i in sorted(data, key=lambda x: x[0]): - item = i[1] - if isinstance(item, Tracks): - self._insert_track(session, item, repaint=False) - elif isinstance(item, Notes): - self._insert_note(session, item, repaint=False) - - # Scroll to top - 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 row - # numbers may have changed during population, and it's cheap to do - self.save_playlist(session) - self.update_display() - def save_playlist(self, session): """ Save playlist to database. @@ -519,17 +561,32 @@ class PlaylistTab(QTableWidget): self._select_tracks(played=False) - def set_selected_as_next(self): + def set_next_track(self, row): """ - Sets the selected track as the next track. + Sets the passed row as the next track. """ - if len(self.selectedItems()) != 1: + if row in self._meta_get_notes(): return - self._set_next(self.currentRow()) + # Update row metadata + self._set_next(row) + # Notify parent + track = self._get_row_object(row) + self.parent.this_is_the_next_track(self, track) + + # Show track as next self.update_display() + def set_selected_as_next(self): + """Sets the select track as next to play""" + + row = self.get_selected_row() + if row is None: + return None + + self.set_next_track(row) + def update_display(self, clear_selection=True): """Set row colours, fonts, etc""" @@ -823,7 +880,7 @@ class PlaylistTab(QTableWidget): try: return datetime.strptime( - text[-Config.NOTE_TIME_FORMAT:], + text[-len(Config.NOTE_TIME_FORMAT):], Config.NOTE_TIME_FORMAT ) except ValueError: @@ -832,7 +889,20 @@ class PlaylistTab(QTableWidget): def _get_row_object(self, row): """Return content associated with this row""" - return self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + # row_item = self.item(row, self.COL_USERDATA) + # obj = row_item.data(self.CONTENT_OBJECT) + # insp = inspect(obj) + # with Session() as session: + # # x = session.query(type(obj)).populate_existing().get(obj.id) + # x = session.get(type(obj), obj.id, populate_existing=True) + # insp = inspect(x) + # insp = inspect(obj) + # insp = inspect(x) + # insp = inspect(obj) + # print(obj) + # return obj + obj = self.item(row, self.COL_USERDATA).data(self.CONTENT_OBJECT) + return obj def _info_row(self, row): """Display popup with info re row""" @@ -859,19 +929,15 @@ class PlaylistTab(QTableWidget): info.setDefaultButton(QMessageBox.Cancel) info.exec() - def _insert_note(self, session, note, repaint=True): + def _insert_note(self, session, note, row=None, repaint=True): """ Insert a note to playlist tab. - If a row is selected, add note above. Otherwise, add to end of + If a row is given, 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: + if row is None: row = self.rowCount() DEBUG(f"playlist.inset_note(): row={row}") @@ -898,72 +964,6 @@ class PlaylistTab(QTableWidget): 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) - # Put an item in COL_USERDATA for later - item = QTableWidgetItem() - self.setItem(row, self.COL_USERDATA, item) - # Add track details to columns - mss_item = QTableWidgetItem(str(track.start_gap)) - if track.start_gap and 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_END_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(title_item, QAbstractItemView.PositionAtCenter) - - if repaint: - self.save_playlist(session) - self.update_display(clear_selection=False) - - return row - def _is_below(self, pos, index): rect = self.visualRect(index) margin = 2 @@ -1091,8 +1091,9 @@ class PlaylistTab(QTableWidget): matches = [] for row in range(self.rowCount()): - if self._meta_get(row) & metadata: - matches.append(row) + if self._meta_get(row): + if self._meta_get(row) & metadata: + matches.append(row) if not one: return matches @@ -1114,7 +1115,11 @@ class PlaylistTab(QTableWidget): if row is None: raise ValueError(f"_meta_set_attribute({row=}, {attribute=})") - new_metadata = self._meta_get(row) | attribute + current_metadata = self._meta_get(row) + if not current_metadata: + new_metadata = attribute + else: + new_metadata = self._meta_get(row) | attribute self.item(row, self.COL_USERDATA).setData( self.ROW_METADATA, new_metadata) @@ -1145,35 +1150,51 @@ class PlaylistTab(QTableWidget): self._meta_set_attribute(row, RowMeta.UNREADABLE) - def _set_next(self, row): + def _populate(self, session): """ - If passed row is track row, check track is readable and, if it is: - - mark that track as the next track to be played - - notify musicmuster - - return track + Populate from the associated playlist object - Otherwise, return None. + 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. """ - DEBUG(f"_set_next({row=})") + data = [] - if row in self._meta_get_notes(): - return None + # Make sure the database object is usable + insp = inspect(self.playlist) + if insp.detached: + session.add(self.playlist) + assert insp.persistent - track = self._get_row_object(row) - if not track: - return None + for row, track in self.playlist.tracks.items(): + insp = inspect(track) + data.append(([row], track)) + # Add track to session to expose attributes + session.add(track) + for note in self.playlist.notes: + data.append(([note.row], note)) - if self._track_is_readable(track): - self._meta_set_next(row) - self.parent.set_next_track(track) - else: - self._meta_set_unreadable(row) - track = None + # Clear playlist + self.setRowCount(0) + + # Now add data in row order + for i in sorted(data, key=lambda x: x[0]): + item = i[1] + if isinstance(item, Tracks): + self.insert_track(session, item, repaint=False) + elif isinstance(item, Notes): + self._insert_note(session, item, repaint=False) + + # Scroll to top + 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 row + # numbers may have changed during population, and it's cheap to do + self.save_playlist(session) self.update_display() - return track - def _rescan(self, row): """ If passed row is track row, rescan it. @@ -1197,10 +1218,22 @@ class PlaylistTab(QTableWidget): If multiple rows are selected, display sum of durations in status bar. """ - rows = set([item.row() for item in self.selectedItems()]) - note_rows = self._meta_get_notes() - ms = sum([self._get_row_object(row).duration - for row in rows if row not in note_rows]) + row_set = set([item.row() for item in self.selectedItems()]) + note_row_set = set(self._meta_get_notes()) + track_rows = list(row_set - note_row_set) + ms = 0 + with Session() as session: + # tracks = [self._get_row_object(row) for row in track_rows] + # # Add all tracks to the session + # [session.add(track) for track in tracks] + for row in track_rows: + track = self._get_row_object(row) + insp = inspect(track) + session.add(track) + insp = inspect(track) + ms += track.duration + # ms = sum([track.duration for track in tracks]) + # Only paint message if there are selected track rows if ms > 0: self.parent.lblSumPlaytime.setText( @@ -1219,6 +1252,35 @@ class PlaylistTab(QTableWidget): else: self.setColumnWidth(column, Config.DEFAULT_COLUMN_WIDTH) + def _set_next(self, row): + """ + If passed row is track row, check track is readable and, if it is: + - mark that track as the next track to be played + - notify musicmuster + - return track + + Otherwise, return None. + """ + + DEBUG(f"_set_next({row=})") + + if row in self._meta_get_notes(): + return None + + track = self._get_row_object(row) + if not track: + return None + + if self._track_is_readable(track): + self._meta_set_next(row) + self.parent.this_is_the_next_track(track) + else: + self._meta_set_unreadable(row) + track = None + self.update_display() + + return track + def _set_row_bold(self, row, bold=True): boldfont = QFont() boldfont.setBold(bold) diff --git a/app/ui/main_window.ui b/app/ui/main_window.ui index 91d312d..899f801 100644 --- a/app/ui/main_window.ui +++ b/app/ui/main_window.ui @@ -794,19 +794,9 @@ border: 1px solid rgb(85, 87, 83); - - - TestMo&de - - - - - - - diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index 0eac72f..4cb9c37 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -356,8 +356,6 @@ class Ui_MainWindow(object): self.menuPlaylist.setObjectName("menuPlaylist") self.menu_Tracks = QtWidgets.QMenu(self.menubar) self.menu_Tracks.setObjectName("menu_Tracks") - self.menuTest = QtWidgets.QMenu(self.menubar) - self.menuTest.setObjectName("menuTest") MainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(MainWindow) self.statusbar.setEnabled(True) @@ -467,14 +465,9 @@ class Ui_MainWindow(object): self.menu_Tracks.addAction(self.action_Resume_previous) self.menu_Tracks.addSeparator() self.menu_Tracks.addAction(self.actionSetNext) - self.menuTest.addAction(self.actionTestFunction) - self.menuTest.addSeparator() - self.menuTest.addAction(self.actionSkipToFade) - self.menuTest.addAction(self.actionSkipToEnd) self.menubar.addAction(self.menuFile.menuAction()) self.menubar.addAction(self.menuPlaylist.menuAction()) self.menubar.addAction(self.menu_Tracks.menuAction()) - self.menubar.addAction(self.menuTest.menuAction()) self.retranslateUi(MainWindow) self.tabPlaylist.setCurrentIndex(-1) @@ -513,7 +506,6 @@ class Ui_MainWindow(object): self.menuFile.setTitle(_translate("MainWindow", "Fi&le")) self.menuPlaylist.setTitle(_translate("MainWindow", "Pla&ylist")) self.menu_Tracks.setTitle(_translate("MainWindow", "&Tracks")) - self.menuTest.setTitle(_translate("MainWindow", "TestMo&de")) self.actionPlay_next.setText(_translate("MainWindow", "&Play next")) self.actionPlay_next.setShortcut(_translate("MainWindow", "Return")) self.actionSkip_next.setText(_translate("MainWindow", "Skip to &next")) diff --git a/poetry.lock b/poetry.lock index 2ffe774..79a583b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -158,11 +158,11 @@ lingua = ["lingua"] [[package]] name = "markupsafe" -version = "2.0.1" +version = "2.1.0" description = "Safely add untrusted strings to HTML/XML markup." category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [[package]] name = "matplotlib-inline" @@ -187,7 +187,7 @@ python-versions = ">=3.5, <4" name = "mypy" version = "0.931" description = "Optional static typing for Python" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -204,7 +204,7 @@ python2 = ["typed-ast (>=1.4.0,<2)"] name = "mypy-extensions" version = "0.4.3" description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" +category = "main" optional = false python-versions = "*" @@ -355,7 +355,7 @@ python-versions = "*" [[package]] name = "pytest" -version = "7.0.0" +version = "7.0.1" description = "pytest: simple powerful testing with Python" category = "dev" optional = false @@ -476,7 +476,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" name = "tomli" version = "2.0.1" description = "A lil' TOML parser" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -495,7 +495,7 @@ test = ["pytest"] name = "typing-extensions" version = "4.0.1" description = "Backported and Experimental Type Hints for Python 3.6+" -category = "dev" +category = "main" optional = false python-versions = ">=3.6" @@ -614,75 +614,46 @@ mako = [ {file = "Mako-1.1.6.tar.gz", hash = "sha256:4e9e345a41924a954251b95b4b28e14a301145b544901332e658907a7464b6b2"}, ] markupsafe = [ - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"}, - {file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"}, - {file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"}, - {file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"}, - {file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"}, - {file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"}, - {file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3028252424c72b2602a323f70fbf50aa80a5d3aa616ea6add4ba21ae9cc9da4c"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:290b02bab3c9e216da57c1d11d2ba73a9f73a614bbdcc027d299a60cdfabb11a"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e104c0c2b4cd765b4e83909cde7ec61a1e313f8a75775897db321450e928cce"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24c3be29abb6b34052fd26fc7a8e0a49b1ee9d282e3665e8ad09a0a68faee5b3"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:204730fd5fe2fe3b1e9ccadb2bd18ba8712b111dcabce185af0b3b5285a7c989"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d3b64c65328cb4cd252c94f83e66e3d7acf8891e60ebf588d7b493a55a1dbf26"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:96de1932237abe0a13ba68b63e94113678c379dca45afa040a17b6e1ad7ed076"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75bb36f134883fdbe13d8e63b8675f5f12b80bb6627f7714c7d6c5becf22719f"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win32.whl", hash = "sha256:4056f752015dfa9828dce3140dbadd543b555afb3252507348c493def166d454"}, + {file = "MarkupSafe-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:d4e702eea4a2903441f2735799d217f4ac1b55f7d8ad96ab7d4e25417cb0827c"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f0eddfcabd6936558ec020130f932d479930581171368fd728efcfb6ef0dd357"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ddea4c352a488b5e1069069f2f501006b1a4362cb906bee9a193ef1245a7a61"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09c86c9643cceb1d87ca08cdc30160d1b7ab49a8a21564868921959bd16441b8"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a0a0abef2ca47b33fb615b491ce31b055ef2430de52c5b3fb19a4042dbc5cadb"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:736895a020e31b428b3382a7887bfea96102c529530299f426bf2e636aacec9e"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:679cbb78914ab212c49c67ba2c7396dc599a8479de51b9a87b174700abd9ea49"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:84ad5e29bf8bab3ad70fd707d3c05524862bddc54dc040982b0dbcff36481de7"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win32.whl", hash = "sha256:8da5924cb1f9064589767b0f3fc39d03e3d0fb5aa29e0cb21d43106519bd624a"}, + {file = "MarkupSafe-2.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:454ffc1cbb75227d15667c09f164a0099159da0c1f3d2636aa648f12675491ad"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:142119fb14a1ef6d758912b25c4e803c3ff66920635c44078666fe7cc3f8f759"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b2a5a856019d2833c56a3dcac1b80fe795c95f401818ea963594b345929dffa7"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d1fb9b2eec3c9714dd936860850300b51dbaa37404209c8d4cb66547884b7ed"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62c0285e91414f5c8f621a17b69fc0088394ccdaa961ef469e833dbff64bd5ea"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fc3150f85e2dbcf99e65238c842d1cfe69d3e7649b19864c1cc043213d9cd730"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f02cf7221d5cd915d7fa58ab64f7ee6dd0f6cddbb48683debf5d04ae9b1c2cc1"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5653619b3eb5cbd35bfba3c12d575db2a74d15e0e1c08bf1db788069d410ce8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7d2f5d97fcbd004c03df8d8fe2b973fe2b14e7bfeb2cfa012eaa8759ce9a762f"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win32.whl", hash = "sha256:3cace1837bc84e63b3fd2dfce37f08f8c18aeb81ef5cf6bb9b51f625cb4e6cd8"}, + {file = "MarkupSafe-2.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:fabbe18087c3d33c5824cb145ffca52eccd053061df1d79d4b66dafa5ad2a5ea"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:023af8c54fe63530545f70dd2a2a7eed18d07a9a77b94e8bf1e2ff7f252db9a3"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d66624f04de4af8bbf1c7f21cc06649c1c69a7f84109179add573ce35e46d448"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c532d5ab79be0199fa2658e24a02fce8542df196e60665dd322409a03db6a52c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e67ec74fada3841b8c5f4c4f197bea916025cb9aa3fe5abf7d52b655d042f956"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c653fde75a6e5eb814d2a0a89378f83d1d3f502ab710904ee585c38888816c"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:961eb86e5be7d0973789f30ebcf6caab60b844203f4396ece27310295a6082c7"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:598b65d74615c021423bd45c2bc5e9b59539c875a9bdb7e5f2a6b92dfcfc268d"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:599941da468f2cf22bf90a84f6e2a65524e87be2fce844f96f2dd9a6c9d1e635"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win32.whl", hash = "sha256:e6f7f3f41faffaea6596da86ecc2389672fa949bd035251eab26dc6697451d05"}, + {file = "MarkupSafe-2.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:b8811d48078d1cf2a6863dafb896e68406c5f513048451cd2ded0473133473c7"}, + {file = "MarkupSafe-2.1.0.tar.gz", hash = "sha256:80beaf63ddfbc64a0452b841d8036ca0611e049650e20afcb882f5d3c266d65f"}, ] matplotlib-inline = [ {file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"}, @@ -841,8 +812,8 @@ pyqtwebengine-qt5 = [ {file = "PyQtWebEngine_Qt5-5.15.2-py3-none-win_amd64.whl", hash = "sha256:24231f19e1595018779977de6722b5c69f3d03f34a5f7574ff21cd1e764ef76d"}, ] pytest = [ - {file = "pytest-7.0.0-py3-none-any.whl", hash = "sha256:42901e6bd4bd4a0e533358a86e848427a49005a3256f657c5c8f8dd35ef137a9"}, - {file = "pytest-7.0.0.tar.gz", hash = "sha256:dad48ffda394e5ad9aa3b7d7ddf339ed502e5e365b1350e0af65f4a602344b11"}, + {file = "pytest-7.0.1-py3-none-any.whl", hash = "sha256:9ce3ff477af913ecf6321fe337b93a2c0dcf2a0a1439c43f5452112c1e4280db"}, + {file = "pytest-7.0.1.tar.gz", hash = "sha256:e30905a0c131d3d94b89624a1cc5afec3e0ba2fbdb151867d8e0ebd49850f171"}, ] pytest-qt = [ {file = "pytest-qt-4.0.2.tar.gz", hash = "sha256:dfc5240dec7eb43b76bcb5f9a87eecae6ef83592af49f3af5f1d5d093acaa93e"}, @@ -922,6 +893,6 @@ wcwidth = [ {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, ] typing-extensions = [ - {file = "typing_extensions-4.1.0-py3-none-any.whl", hash = "sha256:c13180fbaa7cd97065a4915ceba012bdb31dc34743e63ddee16360161d358414"}, - {file = "typing_extensions-4.1.0.tar.gz", hash = "sha256:ba97c5143e5bb067b57793c726dd857b1671d4b02ced273ca0538e71ff009095"}, + {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"}, + {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"}, ] diff --git a/pyproject.toml b/pyproject.toml index 05c2c45..18dd694 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,7 @@ psutil = "^5.9.0" PyQtWebEngine = "^5.15.5" pydub = "^0.25.1" PyQt5-sip = "^12.9.1" +mypy = "^0.931" [tool.poetry.dev-dependencies] mypy = "^0.931" diff --git a/test_models.py b/test_models.py index fcca199..2a78ab6 100644 --- a/test_models.py +++ b/test_models.py @@ -7,6 +7,7 @@ from app.models import ( Notes, Playdates, Playlists, + PlaylistTracks, Tracks, ) @@ -306,6 +307,42 @@ def test_playlist_get_track_playlists(session): assert p2_name not in [a.playlist.name for a in playlists_track2] +def test_playlisttracks_move_track(session): + # We need two playlists + p1_name = "playlist one" + p2_name = "playlist two" + playlist1 = Playlists(session, p1_name) + playlist2 = Playlists(session, p2_name) + + # Need two tracks + track1_row = 17 + track1_path = "/a/b/c" + track1 = Tracks(session, track1_path) + track2_row = 29 + track2_path = "/m/n/o" + track2 = Tracks(session, track2_path) + track1 = Tracks(session, track1_path) + + # Add both to playlist1 and check + playlist1.add_track(session, track1, track1_row) + playlist1.add_track(session, track2, track2_row) + + tracks = playlist1.tracks + assert tracks[track1_row] == track1 + assert tracks[track2_row] == track2 + + # Move track2 to playlist2 and check + PlaylistTracks.move_track( + session, playlist1.id, track2_row, playlist2.id) + + tracks1 = playlist1.tracks + tracks2 = playlist2.tracks + assert len(tracks1) == 1 + assert len(tracks2) == 1 + assert tracks1[track1_row] == track1 + assert tracks2[track2_row] == track2 + + def test_tracks_get_all_paths(session): # Need two tracks track1_path = "/a/b/c" diff --git a/test_playlists.py b/test_playlists.py index 6a9fceb..01e4ce3 100644 --- a/test_playlists.py +++ b/test_playlists.py @@ -1,5 +1,9 @@ +from PyQt5.QtCore import Qt + from app.playlists import Notes, PlaylistTab, Tracks from app.models import Playlists +# from musicmuster import Window +from musicmuster import Window def test_init(qtbot, session): @@ -26,7 +30,7 @@ def test_save_and_restore(qtbot, session): # Add a track track_path = "/a/b/c" track = Tracks(session, track_path) - playlist_tab._insert_track(session, track) + playlist_tab.insert_track(session, track) # Save playlist playlist_tab.save_playlist(session) @@ -37,3 +41,146 @@ def test_save_and_restore(qtbot, session): retrieved_playlist = playlists[0] assert track_path in [a.path for a in retrieved_playlist.tracks.values()] assert note_text in [a.note for a in retrieved_playlist.notes] + + +def test_meta_all_clear(qtbot, session): + + # Create playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + + # Add some tracks + track1_path = "/a/b/c" + track1 = Tracks(session, track1_path) + playlist_tab.insert_track(session, track1) + track2_path = "/d/e/f" + track2 = Tracks(session, track2_path) + playlist_tab.insert_track(session, track2) + track3_path = "/h/i/j" + track3 = Tracks(session, track3_path) + playlist_tab.insert_track(session, track3) + + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [] + assert playlist_tab._meta_get_played() == [] + assert len(playlist_tab._meta_get_unreadable()) == 3 + + +def test_meta(qtbot, session): + + # Create playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + + # Add some tracks + track1_path = "/a/b/c" + track1 = Tracks(session, track1_path) + playlist_tab.insert_track(session, track1) + track2_path = "/d/e/f" + track2 = Tracks(session, track2_path) + playlist_tab.insert_track(session, track2) + track3_path = "/h/i/j" + track3 = Tracks(session, track3_path) + playlist_tab.insert_track(session, track3) + + assert len(playlist_tab._meta_get_unreadable()) == 3 + + assert playlist_tab._meta_get_played() == [] + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [] + + playlist_tab._meta_set_played(0) + assert playlist_tab._meta_get_played() == [0] + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [] + + # Add a note + note_text = "my note" + note_row = 7 # will be added as row 3 + note = Notes(session, playlist.id, note_row, note_text) + playlist_tab._insert_note(session, note) + + assert playlist_tab._meta_get_played() == [0] + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [3] + + playlist_tab._meta_set_next(1) + assert playlist_tab._meta_get_played() == [0] + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() == 1 + assert playlist_tab._meta_get_notes() == [3] + + playlist_tab._meta_set_current(2) + assert playlist_tab._meta_get_played() == [0] + assert playlist_tab._meta_get_current() == 2 + assert playlist_tab._meta_get_next() == 1 + assert playlist_tab._meta_get_notes() == [3] + + playlist_tab._meta_clear_played(0) + assert playlist_tab._meta_get_played() == [] + assert playlist_tab._meta_get_current() == 2 + assert playlist_tab._meta_get_next() == 1 + assert playlist_tab._meta_get_notes() == [3] + + playlist_tab._meta_clear_next() + assert playlist_tab._meta_get_played() == [] + assert playlist_tab._meta_get_current() == 2 + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [3] + + playlist_tab._meta_clear_current() + assert playlist_tab._meta_get_played() == [] + assert playlist_tab._meta_get_current() is None + assert playlist_tab._meta_get_next() is None + assert playlist_tab._meta_get_notes() == [3] + + +def test_clear_next(qtbot, session): + # Create playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + + # Add some tracks + track1_path = "/a/b/c" + track1 = Tracks(session, track1_path) + playlist_tab.insert_track(session, track1) + track2_path = "/d/e/f" + track2 = Tracks(session, track2_path) + playlist_tab.insert_track(session, track2) + + playlist_tab._meta_set_next(1) + assert playlist_tab._meta_get_next() == 1 + + playlist_tab.clear_next() + assert playlist_tab._meta_get_next() is None + + +def test_get_selected_row(qtbot, session): + + # Create playlist + playlist = Playlists(session, "my playlist") + playlist_tab = PlaylistTab(None, session, playlist) + + # Add some tracks + track1_path = "/a/b/c" + track1 = Tracks(session, track1_path) + playlist_tab.insert_track(session, track1) + track2_path = "/d/e/f" + track2 = Tracks(session, track2_path) + playlist_tab.insert_track(session, track2) + + window = Window() + window.show() + qtbot.addWidget(playlist_tab) + qtbot.wait_for_window_shown(playlist_tab) + + row0_item0 = playlist_tab.item(0, 0) + assert row0_item0 is not None + rect = playlist_tab.visualItemRect(row0_item0) + qtbot.mouseClick( + playlist_tab.viewport(), Qt.LeftButton, pos=rect.center() + )