Import rewrite WIP

This commit is contained in:
Keith Edmunds 2025-01-13 15:29:50 +00:00
parent 3b71041b66
commit 4a4058d211
3 changed files with 109 additions and 63 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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: