diff --git a/app/config.py b/app/config.py index ea2c7e2..6a98941 100644 --- a/app/config.py +++ b/app/config.py @@ -126,5 +126,5 @@ class Config(object): # These rely on earlier definitions HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_SECTIONS - IMPORT_DESTINATION = os.path.join(ROOT, "Singles") + IMPORT_DESTINATION = "/tmp/mm" # os.path.join(ROOT, "Singles") REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE) diff --git a/app/file_importer.py b/app/file_importer.py index 3fde1f2..d3c8c8b 100644 --- a/app/file_importer.py +++ b/app/file_importer.py @@ -17,6 +17,7 @@ from PyQt6.QtCore import ( from PyQt6.QtWidgets import ( QButtonGroup, QDialog, + QFileDialog, QHBoxLayout, QLabel, QPushButton, @@ -36,7 +37,7 @@ from config import Config from helpers import ( file_is_unreadable, get_tags, - show_warning, + show_OK, ) from log import log from models import db, Tracks @@ -88,6 +89,9 @@ class DoTrackImport(QObject): shutil.move(self.destination_track_path, temp_file) # Move file to destination shutil.move(self.import_file_path, self.destination_track_path) + # Clean up + if temp_file and os.path.exists(temp_file): + os.unlink(temp_file) with db.Session() as session: self.signals.status_message_signal.emit( @@ -143,10 +147,10 @@ class FileImporter: # Create ModelData if not row_number: row_number = base_model.rowCount() - self.model_data = ModelData(base_model=base_model, row_number=row_number) + self.model_data = ThreadData(base_model=base_model, row_number=row_number) # Place to keep reference to importer threads and data - self.thread_data: dict[QThread, ModelData] = {} + self.thread_data: dict[QThread, ThreadData] = {} # Data structure to track files to import self.import_files_data: dict[str, TrackFileData] = {} @@ -187,7 +191,7 @@ class FileImporter: self.import_files_data[path].tags = get_tags(path) except ApplicationError as e: self.import_files_data[path].import_this_file = False - self.import_files_data[path].error = f"{path} tag errors ({str(e)})" + self.import_files_data[path].error = f"Tag errors ({str(e)})" continue # Get track match data @@ -200,26 +204,17 @@ class FileImporter: # Process user choices self.process_user_choices(path) - # Tell users about files that won't be imported - for path in [ - a - for a in self.import_files_data.keys() - if not self.import_files_data[a].import_this_file - ]: - msg = ( - f"{os.path.basename(path)} will not be imported because " - f"{self.import_files_data[path].error}" - ) - show_warning(None, "File not imported", msg) - - # Import files once we have data for all of them (to avoid long - # drawn-out user interaction while we import files) - for path in [ - a - for a in self.import_files_data.keys() - if self.import_files_data[a].import_this_file - ]: - self._start_thread(path) + # Import files and tell users about files that won't be imported + msgs: list[str] = [] + for (path, entry) in self.import_files_data.items(): + if entry.import_this_file: + self._start_thread(path) + else: + msgs.append( + f"{os.path.basename(path)} will not be imported because {entry.error}" + ) + if msgs: + show_OK("File not imported", "\r\r".join(msgs)) def _start_thread(self, path: str) -> None: """ @@ -230,27 +225,36 @@ class FileImporter: # Create thread and worker thread = QThread() - self.worker = DoTrackImport( + worker = DoTrackImport( associated_thread=thread, import_file_path=path, tags=self.import_files_data[path].tags, destination_path=self.import_files_data[path].destination_path, - track_id=self.import_files_data[path].track_id + track_id=self.import_files_data[path].track_id, ) # Associate data with the thread + self.model_data.worker = worker self.thread_data[thread] = self.model_data - # Move self.worker to thread - self.worker.moveToThread(thread) + # Move worker to thread + worker.moveToThread(thread) + log.debug(f"_start_thread_worker started ({path=}, {id(thread)=}, {id(worker)=})") # Connect signals - thread.started.connect(self.worker.run) - self.worker.import_finished.connect(self._thread_finished) - self.worker.import_finished.connect(thread.quit) - self.worker.import_finished.connect(self.worker.deleteLater) + thread.started.connect(lambda: log.debug(f"Thread {thread} started")) + thread.started.connect(worker.run) + + thread.finished.connect(lambda: log.debug(f"Thread {thread} finished")) thread.finished.connect(thread.deleteLater) + worker.import_finished.connect( + lambda: log.debug(f"Worker task finished for thread {thread}") + ) + worker.import_finished.connect(self._thread_finished) + worker.import_finished.connect(thread.quit) + worker.import_finished.connect(worker.deleteLater) + # Start thread thread.start() @@ -259,6 +263,8 @@ class FileImporter: If track already in playlist, refresh it else insert it """ + log.debug(f" Ending thread {thread}") + model_data = self.thread_data.pop(thread, None) if model_data: if model_data.base_model: @@ -355,22 +361,29 @@ class FileImporter: # enough default = 1 # default choice is import as new track_match_data = self.import_files_data[path].track_match_data - if ( - track_match_data[0].artist_match >= Config.FUZZYMATCH_MINIMUM_SELECT_ARTIST - and track_match_data[0].title_match - >= Config.FUZZYMATCH_MINIMUM_SELECT_TITLE - ): - default = 2 + try: + if track_match_data: + if ( + track_match_data[0].artist_match + >= Config.FUZZYMATCH_MINIMUM_SELECT_ARTIST + and track_match_data[0].title_match + >= Config.FUZZYMATCH_MINIMUM_SELECT_TITLE + ): + default = 2 - for rec in track_match_data: - existing_track_description = (f"{rec.title} ({rec.artist})") - if Config.FUZZYMATCH_SHOW_SCORES: - existing_track_description += f" ({rec.title_match:.0f}%)" - existing_track_path = self._get_existing_track(rec.track_id).path - choices.append( - (existing_track_description, rec.track_id, existing_track_path) - ) + for rec in track_match_data: + existing_track_description = f"{rec.title} ({rec.artist})" + if Config.FUZZYMATCH_SHOW_SCORES: + existing_track_description += f" ({rec.title_match:.0f}%)" + existing_track_path = self._get_existing_track(rec.track_id).path + choices.append( + (existing_track_description, rec.track_id, existing_track_path) + ) + except IndexError: + import pdb + pdb.set_trace() + print(2) dialog = PickMatch( new_track_description=importing_track_description, choices=choices, @@ -383,6 +396,32 @@ class FileImporter: elif dialog.selected_track_id > 0: self.replace_file(path=path, track_id=dialog.selected_track_id) else: + # Import as new, but check destination path doesn't + # already exists + while os.path.exists(self.import_files_data[path].destination_path): + msg = ( + "New import requested but default destination path ({path}) " + "already exists. Click OK and choose where to save this track" + ) + import pdb + + pdb.set_trace() + show_OK(None, title="Desintation path exists", msg=msg) + # Get output filename + pathspec = QFileDialog.getSaveFileName( + None, + "Save imported track", + directory=Config.IMPORT_DESTINATION, + ) + if not pathspec: + self.import_files_data[path].import_this_file = False + self.import_files_data[ + path + ].error = "destination file already exists" + return + + self.import_files_data[path].destination_path = pathspec[0] + self.import_as_new(path=path) else: # User cancelled dialog @@ -398,12 +437,12 @@ class FileImporter: tfd = self.import_files_data[path] - destination_path = os.path.join(Config.IMPORT_DESTINATION, os.path.basename(path)) + destination_path = os.path.join( + Config.IMPORT_DESTINATION, os.path.basename(path) + ) if os.path.exists(destination_path): tfd.import_this_file = False - tfd.error = ( - f"this is a new import but destination file already exists ({destination_path})" - ) + tfd.error = f"this is a new import but destination file already exists ({destination_path})" return tfd.destination_path = destination_path @@ -435,9 +474,10 @@ class FileImporter: @dataclass -class ModelData: +class ThreadData: base_model: PlaylistModel row_number: int + worker: Optional[DoTrackImport] = None class PickMatch(QDialog): diff --git a/app/helpers.py b/app/helpers.py index 637f28f..b0a37dc 100644 --- a/app/helpers.py +++ b/app/helpers.py @@ -10,7 +10,7 @@ import ssl import tempfile # PyQt imports -from PyQt6.QtWidgets import QMainWindow, QMessageBox +from PyQt6.QtWidgets import QMainWindow, QMessageBox, QWidget # Third party imports from mutagen.flac import FLAC # type: ignore @@ -204,14 +204,14 @@ def get_tags(path: str) -> Tags: except TinyTagException: raise ApplicationError(f"Can't read tags: get_tags({path=})") - if not tag.title: - tag.title = "" - if not tag.artist: - tag.artist = "" - if not tag.bitrate: - tag.bitrate = 0.0 - if not tag.duration: - tag.duration = 0.0 + if ( + tag.title is None + or tag.artist is None + or tag.bitrate is None + or tag.duration is None + ): + raise ApplicationError(f"Missing tags: get_tags({path=})") + return Tags( title=tag.title, artist=tag.artist, @@ -400,10 +400,16 @@ def set_track_metadata(track: Tracks) -> None: setattr(track, tag_key, getattr(tags, tag_key)) -def show_OK(parent: QMainWindow, title: str, msg: str) -> None: +def show_OK(title: str, msg: str, parent: Optional[QWidget] = None) -> None: """Display a message to user""" - QMessageBox.information(parent, title, msg, buttons=QMessageBox.StandardButton.Ok) + dlg = QMessageBox(parent) + dlg.setIcon(QMessageBox.Icon.Information) + dlg.setWindowTitle(title) + dlg.setText(msg) + dlg.setStandardButtons(QMessageBox.StandardButton.Ok) + + _ = dlg.exec() def show_warning(parent: Optional[QMainWindow], title: str, msg: str) -> None: