From db547cbdb76b6747dc9ddf591b9066b5739b183a Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 19 Nov 2023 16:02:44 +0000 Subject: [PATCH] WIP V3: import tracks working --- app/classes.py | 2 + app/musicmuster.py | 108 +++++++++++++++---------------------------- app/playlistmodel.py | 71 +++++++++++++++++++++++++++- 3 files changed, 110 insertions(+), 71 deletions(-) diff --git a/app/classes.py b/app/classes.py index fc21b37..2f513f6 100644 --- a/app/classes.py +++ b/app/classes.py @@ -84,6 +84,8 @@ class MusicMusterSignals(QObject): enable_escape_signal = pyqtSignal(bool) next_track_changed_signal = pyqtSignal() span_cells_signal = pyqtSignal(int, int, int, int) + show_warning_signal = pyqtSignal(str, str) + status_message_signal = pyqtSignal(str, int) def __post_init__(self): super().__init__() diff --git a/app/musicmuster.py b/app/musicmuster.py index 064f09d..34b7a2b 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 from datetime import datetime, timedelta -from os.path import basename from time import sleep from typing import ( cast, @@ -145,46 +144,6 @@ class CartButton(QPushButton): self.pgb.setGeometry(0, 0, self.width(), 10) -class ImportTrack(QObject): - import_error = pyqtSignal(str) - importing = pyqtSignal(str) - finished = pyqtSignal(PlaylistTab) - - def __init__(self, playlist: PlaylistTab, filenames: list, row: int) -> None: - super().__init__() - self.filenames = filenames - self.playlist = playlist - self.row = row - - def run(self): - """ - Create track objects from passed files and add to visible playlist - """ - - target_row = self.row - with Session() as session: - for fname in self.filenames: - self.importing.emit(f"Importing {basename(fname)}") - metadata = helpers.get_file_metadata(fname) - try: - track = Tracks(session, **metadata) - except Exception as e: - print(e) - return - helpers.normalise_track(track.path) - self.playlist.insert_track(session, track, target_row) - # Insert next row under this one - target_row += 1 - # We're importing potentially multiple tracks in a loop. - # If there's an error adding the track to the Tracks - # table, the session will rollback, thus losing any - # previous additions in this loop. So, commit now to - # lock in what we've just done. - session.commit() - self.playlist.save_playlist(session) - self.finished.emit(self.playlist) - - class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None, *args, **kwargs) -> None: super().__init__(parent) @@ -476,7 +435,7 @@ class Window(QMainWindow, Ui_MainWindow): closing_tab_playlist_id = self.tabPlaylist.widget(tab_index).playlist_id if current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id: - self.statusbar.showMessage("Can't close current track playlist", 5000) + self.show_status_message("Can't close current track playlist", 5000) return False # Record playlist as closed and update remaining playlist tabs @@ -551,6 +510,8 @@ class Window(QMainWindow, Ui_MainWindow): self.signals.enable_escape_signal.connect(self.enable_escape) self.signals.next_track_changed_signal.connect(self.update_headers) + self.signals.status_message_signal.connect(self.show_status_message) + self.signals.show_warning_signal.connect(self.show_warning) self.timer10.timeout.connect(self.tick_10ms) self.timer500.timeout.connect(self.tick_500ms) @@ -638,7 +599,7 @@ class Window(QMainWindow, Ui_MainWindow): """ self.actionPlay_next.setEnabled(False) - self.statusbar.showMessage("Play controls: Disabled", 0) + self.show_status_message("Play controls: Disabled", 0) def download_played_tracks(self) -> None: """Download a CSV of played tracks""" @@ -689,7 +650,7 @@ class Window(QMainWindow, Ui_MainWindow): """ self.actionPlay_next.setEnabled(True) - self.statusbar.showMessage("Play controls: Enabled", 0) + self.show_status_message("Play controls: Enabled", 0) def export_playlist_tab(self) -> None: """Export the current playlist to an m3u file""" @@ -786,7 +747,21 @@ class Window(QMainWindow, Ui_MainWindow): txt = "" tags = helpers.get_tags(fname) title = tags["title"] + if not title: + helpers.show_warning( + self, + "Problem with track file", + f"{fname} does not have a title tag", + ) + continue artist = tags["artist"] + if not artist: + helpers.show_warning( + self, + "Problem with track file", + f"{fname} does not have an artist tag", + ) + continue count = 0 possible_matches = Tracks.search_titles(session, title) if possible_matches: @@ -813,33 +788,10 @@ class Window(QMainWindow, Ui_MainWindow): continue new_tracks.append(fname) - # Import in separate thread - self.import_thread = QThread() - self.worker = ImportTrack( - self.active_tab(), - new_tracks, - self.active_tab().get_new_row_number(), + # Pass to model to manage the import + self.active_model().import_tracks( + new_tracks, self.active_tab().get_selected_row_number() ) - self.worker.moveToThread(self.import_thread) - self.import_thread.started.connect(self.worker.run) - self.worker.finished.connect(self.import_thread.quit) - self.worker.finished.connect(self.worker.deleteLater) - self.import_thread.finished.connect(self.import_thread.deleteLater) - self.worker.import_error.connect( - lambda msg: helpers.show_warning( - self, "Import error", "Error importing " + msg - ) - ) - self.worker.importing.connect(lambda msg: self.statusbar.showMessage(msg, 5000)) - self.worker.finished.connect(self.import_complete) - self.import_thread.start() - - def import_complete(self): - """ - Called by thread when track import complete - """ - - self.statusbar.showMessage("Imports complete") def insert_header(self) -> None: """Show dialog box to enter header text and add to playlist""" @@ -1355,6 +1307,14 @@ class Window(QMainWindow, Ui_MainWindow): # self.tabPlaylist.setCurrentWidget(self.current_track.playlist_tab) # self.tabPlaylist.currentWidget().scroll_current_to_top() + def show_warning(self, title: str, body: str) -> None: + """ + Display a warning dialog + """ + + print(f"show_warning({title=}, {body=})") + QMessageBox.warning(self, title, body) + def show_next(self) -> None: """Scroll to show next track""" @@ -1364,6 +1324,14 @@ class Window(QMainWindow, Ui_MainWindow): # self.tabPlaylist.setCurrentWidget(self.next_track.playlist_tab) # self.tabPlaylist.currentWidget().scroll_next_to_top() + def show_status_message(self, message: str, timing: int) -> None: + """ + Show status message in status bar for timing milliseconds + """ + + print(f"show_status_message({message=}, {timing=})") + self.statusbar.showMessage(message, timing) + def solicit_playlist_name( self, session: scoped_session, default: str = "" ) -> Optional[str]: diff --git a/app/playlistmodel.py b/app/playlistmodel.py index e02a0ea..563f1c8 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -2,12 +2,16 @@ from dataclasses import dataclass from datetime import datetime, timedelta from enum import auto, Enum from operator import attrgetter +from os.path import basename from typing import List, Optional from PyQt6.QtCore import ( + pyqtSignal, QAbstractTableModel, QModelIndex, + QObject, Qt, + QThread, QVariant, ) from PyQt6.QtGui import ( @@ -22,8 +26,10 @@ from dbconfig import scoped_session, Session from helpers import ( file_is_unreadable, get_embedded_time, + get_file_metadata, get_relative_date, open_in_audacity, + normalise_track, ms_to_mmss, set_track_metadata, ) @@ -150,7 +156,9 @@ class PlaylistModel(QAbstractTableModel): if playlist_id != self.playlist_id: return - self.insert_row(proposed_row_number=new_row_number, track_id=track_id, note=note) + self.insert_row( + proposed_row_number=new_row_number, track_id=track_id, note=note + ) def add_track_to_header( self, @@ -634,6 +642,28 @@ class PlaylistModel(QAbstractTableModel): return prd.note + def import_tracks( + self, new_tracks: List[str], proposed_row_number: Optional[int] + ) -> None: + """ + Import the file paths listed in new_tracks. The files have already been sanitised + so no further checks needed. Import in a separate thread as this is slow. + """ + + # Import in separate thread + self.import_thread = QThread() + self.worker = ImportTrack( + self, + new_tracks, + proposed_row_number, + ) + self.worker.moveToThread(self.import_thread) + self.import_thread.started.connect(self.worker.run) + self.worker.import_finished.connect(self.import_thread.quit) + self.worker.import_finished.connect(self.worker.deleteLater) + self.import_thread.finished.connect(self.import_thread.deleteLater) + self.import_thread.start() + def is_header_row(self, row_number: int) -> bool: """ Return True if row is a header row, else False @@ -1079,3 +1109,42 @@ class PlaylistModel(QAbstractTableModel): self.index(updated_row, Col.START_TIME.value), self.index(updated_row, Col.END_TIME.value), ) + + +class ImportTrack(QObject): + import_finished = pyqtSignal() + + def __init__(self, model: PlaylistModel, filenames: List[str], row_number) -> None: + super().__init__() + self.model = model + self.filenames = filenames + self.row_number = row_number + self.signals = MusicMusterSignals() + + def run(self): + """ + Create track objects from passed files and add to visible playlist + """ + + target_row = self.row_number + with Session() as session: + for fname in self.filenames: + self.signals.status_message_signal.emit(f"Importing {basename(fname)}", 5000) + metadata = get_file_metadata(fname) + try: + track = Tracks(session, **metadata) + except Exception as e: + self.signals.show_warning_signal.emit("Error importing track", e) + return + normalise_track(track.path) + self.model.insert_row(self.row_number, track.id) + # Insert next row under this one + target_row += 1 + # We're importing potentially multiple tracks in a loop. + # If there's an error adding the track to the Tracks + # table, the session will rollback, thus losing any + # previous additions in this loop. So, commit now to + # lock in what we've just done. + session.commit() + self.signals.status_message_signal.emit(f"{len(self.filenames)} tracks imported", 10000) + self.import_finished.emit()