Import rewrite WIP
This commit is contained in:
parent
3b71041b66
commit
4a4058d211
@ -126,5 +126,5 @@ class Config(object):
|
|||||||
|
|
||||||
# These rely on earlier definitions
|
# These rely on earlier definitions
|
||||||
HIDE_PLAYED_MODE = HIDE_PLAYED_MODE_SECTIONS
|
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)
|
REPLACE_FILES_DEFAULT_DESTINATION = os.path.dirname(REPLACE_FILES_DEFAULT_SOURCE)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from PyQt6.QtCore import (
|
|||||||
from PyQt6.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QButtonGroup,
|
QButtonGroup,
|
||||||
QDialog,
|
QDialog,
|
||||||
|
QFileDialog,
|
||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
QLabel,
|
QLabel,
|
||||||
QPushButton,
|
QPushButton,
|
||||||
@ -36,7 +37,7 @@ from config import Config
|
|||||||
from helpers import (
|
from helpers import (
|
||||||
file_is_unreadable,
|
file_is_unreadable,
|
||||||
get_tags,
|
get_tags,
|
||||||
show_warning,
|
show_OK,
|
||||||
)
|
)
|
||||||
from log import log
|
from log import log
|
||||||
from models import db, Tracks
|
from models import db, Tracks
|
||||||
@ -88,6 +89,9 @@ class DoTrackImport(QObject):
|
|||||||
shutil.move(self.destination_track_path, temp_file)
|
shutil.move(self.destination_track_path, temp_file)
|
||||||
# Move file to destination
|
# Move file to destination
|
||||||
shutil.move(self.import_file_path, self.destination_track_path)
|
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:
|
with db.Session() as session:
|
||||||
self.signals.status_message_signal.emit(
|
self.signals.status_message_signal.emit(
|
||||||
@ -143,10 +147,10 @@ class FileImporter:
|
|||||||
# Create ModelData
|
# Create ModelData
|
||||||
if not row_number:
|
if not row_number:
|
||||||
row_number = base_model.rowCount()
|
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
|
# 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
|
# Data structure to track files to import
|
||||||
self.import_files_data: dict[str, TrackFileData] = {}
|
self.import_files_data: dict[str, TrackFileData] = {}
|
||||||
@ -187,7 +191,7 @@ class FileImporter:
|
|||||||
self.import_files_data[path].tags = get_tags(path)
|
self.import_files_data[path].tags = get_tags(path)
|
||||||
except ApplicationError as e:
|
except ApplicationError as e:
|
||||||
self.import_files_data[path].import_this_file = False
|
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
|
continue
|
||||||
|
|
||||||
# Get track match data
|
# Get track match data
|
||||||
@ -200,26 +204,17 @@ class FileImporter:
|
|||||||
# Process user choices
|
# Process user choices
|
||||||
self.process_user_choices(path)
|
self.process_user_choices(path)
|
||||||
|
|
||||||
# Tell users about files that won't be imported
|
# Import files and tell users about files that won't be imported
|
||||||
for path in [
|
msgs: list[str] = []
|
||||||
a
|
for (path, entry) in self.import_files_data.items():
|
||||||
for a in self.import_files_data.keys()
|
if entry.import_this_file:
|
||||||
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)
|
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:
|
def _start_thread(self, path: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -230,27 +225,36 @@ class FileImporter:
|
|||||||
|
|
||||||
# Create thread and worker
|
# Create thread and worker
|
||||||
thread = QThread()
|
thread = QThread()
|
||||||
self.worker = DoTrackImport(
|
worker = DoTrackImport(
|
||||||
associated_thread=thread,
|
associated_thread=thread,
|
||||||
import_file_path=path,
|
import_file_path=path,
|
||||||
tags=self.import_files_data[path].tags,
|
tags=self.import_files_data[path].tags,
|
||||||
destination_path=self.import_files_data[path].destination_path,
|
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
|
# Associate data with the thread
|
||||||
|
self.model_data.worker = worker
|
||||||
self.thread_data[thread] = self.model_data
|
self.thread_data[thread] = self.model_data
|
||||||
|
|
||||||
# Move self.worker to thread
|
# Move worker to thread
|
||||||
self.worker.moveToThread(thread)
|
worker.moveToThread(thread)
|
||||||
|
log.debug(f"_start_thread_worker started ({path=}, {id(thread)=}, {id(worker)=})")
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
thread.started.connect(self.worker.run)
|
thread.started.connect(lambda: log.debug(f"Thread {thread} started"))
|
||||||
self.worker.import_finished.connect(self._thread_finished)
|
thread.started.connect(worker.run)
|
||||||
self.worker.import_finished.connect(thread.quit)
|
|
||||||
self.worker.import_finished.connect(self.worker.deleteLater)
|
thread.finished.connect(lambda: log.debug(f"Thread {thread} finished"))
|
||||||
thread.finished.connect(thread.deleteLater)
|
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
|
# Start thread
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
@ -259,6 +263,8 @@ class FileImporter:
|
|||||||
If track already in playlist, refresh it else insert it
|
If track already in playlist, refresh it else insert it
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
log.debug(f" Ending thread {thread}")
|
||||||
|
|
||||||
model_data = self.thread_data.pop(thread, None)
|
model_data = self.thread_data.pop(thread, None)
|
||||||
if model_data:
|
if model_data:
|
||||||
if model_data.base_model:
|
if model_data.base_model:
|
||||||
@ -355,22 +361,29 @@ class FileImporter:
|
|||||||
# enough
|
# enough
|
||||||
default = 1 # default choice is import as new
|
default = 1 # default choice is import as new
|
||||||
track_match_data = self.import_files_data[path].track_match_data
|
track_match_data = self.import_files_data[path].track_match_data
|
||||||
|
try:
|
||||||
|
if track_match_data:
|
||||||
if (
|
if (
|
||||||
track_match_data[0].artist_match >= Config.FUZZYMATCH_MINIMUM_SELECT_ARTIST
|
track_match_data[0].artist_match
|
||||||
|
>= Config.FUZZYMATCH_MINIMUM_SELECT_ARTIST
|
||||||
and track_match_data[0].title_match
|
and track_match_data[0].title_match
|
||||||
>= Config.FUZZYMATCH_MINIMUM_SELECT_TITLE
|
>= Config.FUZZYMATCH_MINIMUM_SELECT_TITLE
|
||||||
):
|
):
|
||||||
default = 2
|
default = 2
|
||||||
|
|
||||||
for rec in track_match_data:
|
for rec in track_match_data:
|
||||||
existing_track_description = (f"{rec.title} ({rec.artist})")
|
existing_track_description = f"{rec.title} ({rec.artist})"
|
||||||
if Config.FUZZYMATCH_SHOW_SCORES:
|
if Config.FUZZYMATCH_SHOW_SCORES:
|
||||||
existing_track_description += f" ({rec.title_match:.0f}%)"
|
existing_track_description += f" ({rec.title_match:.0f}%)"
|
||||||
existing_track_path = self._get_existing_track(rec.track_id).path
|
existing_track_path = self._get_existing_track(rec.track_id).path
|
||||||
choices.append(
|
choices.append(
|
||||||
(existing_track_description, rec.track_id, existing_track_path)
|
(existing_track_description, rec.track_id, existing_track_path)
|
||||||
)
|
)
|
||||||
|
except IndexError:
|
||||||
|
import pdb
|
||||||
|
|
||||||
|
pdb.set_trace()
|
||||||
|
print(2)
|
||||||
dialog = PickMatch(
|
dialog = PickMatch(
|
||||||
new_track_description=importing_track_description,
|
new_track_description=importing_track_description,
|
||||||
choices=choices,
|
choices=choices,
|
||||||
@ -383,6 +396,32 @@ class FileImporter:
|
|||||||
elif dialog.selected_track_id > 0:
|
elif dialog.selected_track_id > 0:
|
||||||
self.replace_file(path=path, track_id=dialog.selected_track_id)
|
self.replace_file(path=path, track_id=dialog.selected_track_id)
|
||||||
else:
|
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)
|
self.import_as_new(path=path)
|
||||||
else:
|
else:
|
||||||
# User cancelled dialog
|
# User cancelled dialog
|
||||||
@ -398,12 +437,12 @@ class FileImporter:
|
|||||||
|
|
||||||
tfd = self.import_files_data[path]
|
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):
|
if os.path.exists(destination_path):
|
||||||
tfd.import_this_file = False
|
tfd.import_this_file = False
|
||||||
tfd.error = (
|
tfd.error = f"this is a new import but destination file already exists ({destination_path})"
|
||||||
f"this is a new import but destination file already exists ({destination_path})"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
tfd.destination_path = destination_path
|
tfd.destination_path = destination_path
|
||||||
@ -435,9 +474,10 @@ class FileImporter:
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ModelData:
|
class ThreadData:
|
||||||
base_model: PlaylistModel
|
base_model: PlaylistModel
|
||||||
row_number: int
|
row_number: int
|
||||||
|
worker: Optional[DoTrackImport] = None
|
||||||
|
|
||||||
|
|
||||||
class PickMatch(QDialog):
|
class PickMatch(QDialog):
|
||||||
|
|||||||
@ -10,7 +10,7 @@ import ssl
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
# PyQt imports
|
# PyQt imports
|
||||||
from PyQt6.QtWidgets import QMainWindow, QMessageBox
|
from PyQt6.QtWidgets import QMainWindow, QMessageBox, QWidget
|
||||||
|
|
||||||
# Third party imports
|
# Third party imports
|
||||||
from mutagen.flac import FLAC # type: ignore
|
from mutagen.flac import FLAC # type: ignore
|
||||||
@ -204,14 +204,14 @@ def get_tags(path: str) -> Tags:
|
|||||||
except TinyTagException:
|
except TinyTagException:
|
||||||
raise ApplicationError(f"Can't read tags: get_tags({path=})")
|
raise ApplicationError(f"Can't read tags: get_tags({path=})")
|
||||||
|
|
||||||
if not tag.title:
|
if (
|
||||||
tag.title = ""
|
tag.title is None
|
||||||
if not tag.artist:
|
or tag.artist is None
|
||||||
tag.artist = ""
|
or tag.bitrate is None
|
||||||
if not tag.bitrate:
|
or tag.duration is None
|
||||||
tag.bitrate = 0.0
|
):
|
||||||
if not tag.duration:
|
raise ApplicationError(f"Missing tags: get_tags({path=})")
|
||||||
tag.duration = 0.0
|
|
||||||
return Tags(
|
return Tags(
|
||||||
title=tag.title,
|
title=tag.title,
|
||||||
artist=tag.artist,
|
artist=tag.artist,
|
||||||
@ -400,10 +400,16 @@ def set_track_metadata(track: Tracks) -> None:
|
|||||||
setattr(track, tag_key, getattr(tags, tag_key))
|
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"""
|
"""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:
|
def show_warning(parent: Optional[QMainWindow], title: str, msg: str) -> None:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user