Revert "Add duplicate entry handling (Fix #179)"

This reverts commit 491ebb6714.
This commit is contained in:
Travis Abendshien 2024-05-20 17:43:25 -07:00
parent 6357fea8db
commit 66ec0913b6
7 changed files with 325 additions and 279 deletions

View file

@ -86,7 +86,7 @@ class Entry:
return self.__str__()
def __eq__(self, __value: object) -> bool:
# __value = cast(Self, object)
__value = cast(Self, object)
if os.name == "nt":
return (
int(self.id) == int(__value.id) #type: ignore
@ -328,7 +328,7 @@ class Library:
def __init__(self) -> None:
# Library Info =========================================================
self.library_dir: Path = None
self.library_dir: str = None
# Entries ==============================================================
# List of every Entry object.
@ -439,7 +439,7 @@ class Library:
{"id": 30, "name": "Comments", "type": "text_box"},
]
def create_library(self, path: Path) -> int:
def create_library(self, path) -> int:
"""
Creates a TagStudio library in the given directory.\n
Return Codes:\n
@ -447,6 +447,8 @@ class Library:
2: File creation error
"""
path = os.path.normpath(path).rstrip("\\")
# If '.TagStudio' is included in the path, trim the path up to it.
if TS_FOLDER_NAME in str(path):
# TODO: Native Path method instead of this casting.
@ -467,12 +469,12 @@ class Library:
def verify_ts_folders(self) -> None:
"""Verifies/creates folders required by TagStudio."""
full_ts_path = Path() / self.library_dir / TS_FOLDER_NAME
full_backup_path = (
Path() / self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME
full_ts_path = os.path.normpath(f"{self.library_dir}/{TS_FOLDER_NAME}")
full_backup_path = os.path.normpath(
f"{self.library_dir}/{TS_FOLDER_NAME}/{BACKUP_FOLDER_NAME}"
)
full_collage_path = (
Path() / self.library_dir / TS_FOLDER_NAME / COLLAGE_FOLDER_NAME
full_collage_path = os.path.normpath(
f"{self.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}"
)
if not os.path.isdir(full_ts_path):
@ -521,7 +523,7 @@ class Library:
encoding="utf-8",
) as file:
json_dump: JsonLibary = ujson.load(file)
self.library_dir = Path(path)
self.library_dir = str(path)
self.verify_ts_folders()
major, minor, patch = json_dump["ts-version"].split(".")
@ -677,7 +679,6 @@ class Library:
)
self.entries.append(e)
self._map_entry_id_to_index(e, -1)
end_time = time.time()
logging.info(
f"[LIBRARY] Entries loaded in {(end_time - start_time):.3f} seconds"
@ -739,9 +740,19 @@ class Library:
"""Maps a full filepath to its corresponding Entry's ID."""
self.filename_to_entry_id_map.clear()
for entry in self.entries:
self.filename_to_entry_id_map[Path() / entry.path / entry.filename] = (
entry.id
)
if os.name == "nt":
# print(str(os.path.normpath(
# f'{entry.path}/{entry.filename}')).lower().lstrip('\\').lstrip('/'))
self.filename_to_entry_id_map[
str(os.path.normpath(f"{entry.path}/{entry.filename}"))
.lower()
.lstrip("\\")
.lstrip("/")
] = entry.id
else:
self.filename_to_entry_id_map[
str(os.path.normpath(f"{entry.path}/{entry.filename}")).lstrip("/")
] = entry.id
# def _map_filenames_to_entry_ids(self):
# """Maps the file paths of entries to their index in the library list."""
@ -891,19 +902,23 @@ class Library:
# - Files without library entries
# for type in TYPES:
start_time = time.time()
for f in self.library_dir.glob("**/*"):
for f in glob.glob(self.library_dir + "/**/*", recursive=True):
# p = Path(os.path.normpath(f))
if (
"$RECYCLE.BIN" not in f.parts
and TS_FOLDER_NAME not in f.parts
and "tagstudio_thumbs" not in f.parts
and not f.is_dir()
"$RECYCLE.BIN" not in f
and TS_FOLDER_NAME not in f
and "tagstudio_thumbs" not in f
and not os.path.isdir(f)
):
if f.suffix not in self.ignored_extensions:
if os.path.splitext(f)[1][1:].lower() not in self.ignored_extensions:
self.dir_file_count += 1
file = f.relative_to(self.library_dir)
file = str(os.path.relpath(f, self.library_dir))
try:
_ = self.filename_to_entry_id_map[file]
if os.name == "nt":
_ = self.filename_to_entry_id_map[file.lower()]
else:
_ = self.filename_to_entry_id_map[file]
except KeyError:
# print(file)
self.files_not_in_library.append(file)
@ -922,7 +937,9 @@ class Library:
try:
self.files_not_in_library = sorted(
self.files_not_in_library,
key=lambda t: -os.stat((Path() / self.library_dir / t)).st_ctime,
key=lambda t: -os.stat(
os.path.normpath(self.library_dir + "/" + t)
).st_ctime,
)
except (FileExistsError, FileNotFoundError):
print(
@ -953,7 +970,12 @@ class Library:
# Step [1/2]:
# Remove this Entry from the Entries list.
entry = self.get_entry(entry_id)
path = Path() / entry.path / entry.filename
path = (
str(os.path.normpath(f"{entry.path}/{entry.filename}"))
.lstrip("\\")
.lstrip("/")
)
path = path.lower() if os.name == "nt" else path
# logging.info(f'Removing path: {path}')
del self.filename_to_entry_id_map[path]
@ -979,34 +1001,51 @@ class Library:
`dupe_entries = tuple(int, list[int])`
"""
# self.dupe_entries.clear()
# known_files: set = set()
# for entry in self.entries:
# full_path = os.path.normpath(f'{self.library_dir}/{entry.path}/{entry.filename}')
# if full_path in known_files:
# self.dupe_entries.append(full_path)
# else:
# known_files.add(full_path)
self.dupe_entries.clear()
registered: dict = {} # string: list[int]
# Registered: filename : list[ALL entry IDs pointing to this filename]
# Dupe Entries: primary ID : list of [every OTHER entry ID pointing]
for i, e in enumerate(self.entries):
file: Path = Path() / e.path / e.filename
# If this unique filepath has not been marked as checked,
if not registered.get(file, None):
# Register the filepath as having been checked, and include
# its entry ID as the first entry in the corresponding list.
registered[file] = [e.id]
# Else if the filepath is already been seen in another entry,
else:
# Add this new entry ID to the list of entry ID(s) pointing to
# the same file.
registered[file].append(e.id)
yield i - 1 # The -1 waits for the next step to finish
for k, v in registered.items():
if len(v) > 1:
self.dupe_entries.append((v[0], v[1:]))
# logging.info(f"DUPLICATE FOUND: {(v[0], v[1:])}")
# for id in v:
# logging.info(f"\t{(Path()/self.get_entry(id).path/self.get_entry(id).filename)}")
yield len(self.entries)
checked = set()
remaining: list[Entry] = list(self.entries)
for p, entry_p in enumerate(self.entries, start=0):
if p not in checked:
matched: list[int] = []
for c, entry_c in enumerate(remaining, start=0):
if os.name == "nt":
if (
entry_p.path.lower() == entry_c.path.lower()
and entry_p.filename.lower() == entry_c.filename.lower()
and c != p
):
matched.append(c)
checked.add(c)
else:
if (
entry_p.path == entry_c.path
and entry_p.filename == entry_c.filename
and c != p
):
matched.append(c)
checked.add(c)
if matched:
self.dupe_entries.append((p, matched))
sys.stdout.write(
f"\r[LIBRARY] Entry [{p}/{len(self.entries)-1}]: Has Duplicate(s): {matched}"
)
sys.stdout.flush()
else:
sys.stdout.write(
f"\r[LIBRARY] Entry [{p}/{len(self.entries)-1}]: Has No Duplicates"
)
sys.stdout.flush()
checked.add(p)
print("")
def merge_dupe_entries(self):
"""
@ -1016,36 +1055,35 @@ class Library:
`dupe_entries = tuple(int, list[int])`
"""
logging.info("[LIBRARY] Mirroring Duplicate Entries...")
id_to_entry_map: dict = {}
print("[LIBRARY] Mirroring Duplicate Entries...")
for dupe in self.dupe_entries:
# Store the id to entry relationship as the library one is about to
# be destroyed.
# NOTE: This is not a good solution, but will be upended by the
# database migration soon anyways.
for id in dupe[1]:
id_to_entry_map[id] = self.get_entry(id)
self.mirror_entry_fields([dupe[0]] + dupe[1])
logging.info(
# print('Consolidating Entries...')
# for dupe in self.dupe_entries:
# for index in dupe[1]:
# print(f'Consolidating Duplicate: {(self.entries[index].path + os.pathsep + self.entries[index].filename)}')
# self.entries.remove(self.entries[index])
# self._map_filenames_to_entry_indices()
print(
"[LIBRARY] Consolidating Entries... (This may take a while for larger libraries)"
)
for i, dupe in enumerate(self.dupe_entries):
for id in dupe[1]:
# NOTE: Instead of using self.remove_entry(id), I'm bypassing it
# because it's currently inefficient in how it needs to remap
# every ID to every list index. I'm recreating the steps it
# takes but in a batch-friendly way here.
# NOTE: Couldn't use get_entry(id) because that relies on the
# entry's index in the list, which is currently being messed up.
logging.info(f"[LIBRARY] Removing Unneeded Entry {id}")
self.entries.remove(id_to_entry_map[id])
yield i - 1 # The -1 waits for the next step to finish
self._entry_id_to_index_map.clear()
for i, e in enumerate(self.entries, start=0):
self._map_entry_id_to_index(e, i)
unique: list[Entry] = []
for i, e in enumerate(self.entries):
if e not in unique:
unique.append(e)
# print(f'[{i}/{len(self.entries)}] Appending: {(e.path + os.pathsep + e.filename)[0:32]}...')
sys.stdout.write(
f"\r[LIBRARY] [{i}/{len(self.entries)}] Appending Unique Entry..."
)
else:
sys.stdout.write(
f"\r[LIBRARY] [{i}/{len(self.entries)}] Consolidating Duplicate: {(e.path + os.pathsep + e.filename)[0:]}..."
)
print("")
# [unique.append(x) for x in self.entries if x not in unique]
self.entries = unique
self._map_filenames_to_entry_ids()
def refresh_dupe_files(self, results_filepath):
@ -1055,9 +1093,9 @@ class Library:
by a DupeGuru results file.
"""
full_results_path = (
Path() / self.library_dir / results_filepath
os.path.normpath(f"{self.library_dir}/{results_filepath}")
if self.library_dir not in results_filepath
else Path(results_filepath)
else os.path.normpath(f"{results_filepath}")
)
if os.path.exists(full_results_path):
self.dupe_files.clear()
@ -1083,15 +1121,26 @@ class Library:
)
for match in matches:
# print(f'MATCHED ({match[2]}%): \n {files[match[0]]} \n-> {files[match[1]]}')
file_1 = Path() / files[match[0]] / self.library_dir
file_2 = Path() / files[match[1]] / self.library_dir
if (
file_1 in self.filename_to_entry_id_map.keys()
and file_2 in self.filename_to_entry_id_map.keys()
):
self.dupe_files.append(
(files[match[0]], files[match[1]], match[2])
)
if os.name == "nt":
file_1 = str(os.path.relpath(files[match[0]], self.library_dir))
file_2 = str(os.path.relpath(files[match[1]], self.library_dir))
if (
file_1.lower() in self.filename_to_entry_id_map.keys()
and file_2.lower() in self.filename_to_entry_id_map.keys()
):
self.dupe_files.append(
(files[match[0]], files[match[1]], match[2])
)
else:
if (
file_1 in self.filename_to_entry_id_map.keys()
and file_2 in self.filename_to_entry_id_map.keys()
):
self.dupe_files.append(
(files[match[0]], files[match[1]], match[2])
)
# self.dupe_files.append((files[match[0]], files[match[1]], match[2]))
print("")
for dupe in self.dupe_files:
@ -1167,16 +1216,19 @@ class Library:
print(f"[LIBRARY] Fixed {self.get_entry(id).filename}")
# (int, str)
# Consolidate new matches with existing unlinked entries.
self.refresh_dupe_entries()
if self.dupe_entries:
self.merge_dupe_entries()
# Remap filenames to entry IDs.
self._map_filenames_to_entry_ids()
# TODO - the type here doesnt match but I cant reproduce calling this
self.remove_missing_matches(fixed_indices)
# for i in fixed_indices:
# # print(json_dump[i])
# del self.missing_matches[i]
# with open(matched_json_filepath, "w") as outfile:
# outfile.flush()
# json.dump({}, outfile, indent=4)
# print(f'Re-saved to disk at {matched_json_filepath}')
def _match_missing_file(self, file: str) -> list[str]:
"""
Tries to find missing entry files within the library directory.
@ -1204,7 +1256,7 @@ class Library:
# matches[file].append(new_path)
print(
f"[LIBRARY] MATCH: {file} \n\t-> {Path()/self.library_dir/new_path/tail}\n"
f'[LIBRARY] MATCH: {file} \n\t-> {os.path.normpath(self.library_dir + "/" + new_path + "/" + tail)}\n'
)
if not matches:
@ -1293,12 +1345,20 @@ class Library:
return None
# @deprecated('Use new Entry ID system.')
def get_entry_id_from_filepath(self, filename: Path):
def get_entry_id_from_filepath(self, filename):
"""Returns an Entry ID given the full filepath it points to."""
try:
if self.entries:
if os.name == "nt":
return self.filename_to_entry_id_map[
str(
os.path.normpath(
os.path.relpath(filename, self.library_dir)
)
).lower()
]
return self.filename_to_entry_id_map[
Path(filename).relative_to(self.library_dir)
str(os.path.normpath(os.path.relpath(filename, self.library_dir)))
]
except:
return -1

View file

@ -32,7 +32,7 @@ class DeleteUnlinkedEntriesModal(QWidget):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle("Delete Unlinked Entries")
self.setWindowTitle(f"Delete Unlinked Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
@ -81,6 +81,20 @@ class DeleteUnlinkedEntriesModal(QWidget):
self.model.appendRow(QStandardItem(i))
def delete_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Deleting Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.lib.ref(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.remove_missing_files)
pw = ProgressWidget(
@ -105,3 +119,23 @@ class DeleteUnlinkedEntriesModal(QWidget):
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
# def delete_entries_runnable(self):
# deleted = []
# for i, missing in enumerate(self.lib.missing_files):
# # pb.setValue(i)
# # pb.setLabelText(f'Deleting {i}/{len(self.lib.missing_files)} Unlinked Entries')
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# yield i
# for d in deleted:
# self.lib.missing_files.remove(d)
# # self.driver.filter_items('')
# # self.done.emit()

View file

@ -6,7 +6,7 @@
import logging
import typing
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtCore import QThread, Qt, QThreadPool
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from src.core.library import Library
@ -14,7 +14,6 @@ from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.modals.delete_unlinked import DeleteUnlinkedEntriesModal
from src.qt.modals.relink_unlinked import RelinkUnlinkedEntries
from src.qt.modals.merge_dupe_entries import MergeDuplicateEntries
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
@ -22,79 +21,65 @@ if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
ERROR = "[ERROR]"
WARNING = "[WARNING]"
INFO = "[INFO]"
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
logging.basicConfig(format="%(message)s", level=logging.INFO)
class FixUnlinkedEntriesModal(QWidget):
# done = Signal(int)
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
self.missing_count = -1
self.dupe_count = -1
self.setWindowTitle("Fix Unlinked Entries")
self.count = -1
self.setWindowTitle(f"Fix Unlinked Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(400, 300)
self.root_layout = QVBoxLayout(self)
self.root_layout.setContentsMargins(6, 6, 6, 6)
self.unlinked_desc_widget = QLabel()
self.unlinked_desc_widget.setObjectName("unlinkedDescriptionLabel")
self.unlinked_desc_widget.setWordWrap(True)
self.unlinked_desc_widget.setStyleSheet("text-align:left;")
self.unlinked_desc_widget.setText(
"""Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked. Unlinked entries may be automatically relinked via searching your directories, manually relinked by the user, or deleted if desired."""
self.desc_widget = QLabel()
self.desc_widget.setObjectName("descriptionLabel")
self.desc_widget.setWordWrap(True)
self.desc_widget.setStyleSheet(
# 'background:blue;'
"text-align:left;"
# 'font-weight:bold;'
# 'font-size:14px;'
# 'padding-top: 6px'
""
)
self.desc_widget.setText("""Each library entry is linked to a file in one of your directories. If a file linked to an entry is moved or deleted outside of TagStudio, it is then considered unlinked.
Unlinked entries may be automatically relinked via searching your directories, manually relinked by the user, or deleted if desired.""")
self.desc_widget.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.dupe_desc_widget = QLabel()
self.dupe_desc_widget.setObjectName("dupeDescriptionLabel")
self.dupe_desc_widget.setWordWrap(True)
self.dupe_desc_widget.setStyleSheet("text-align:left;")
self.dupe_desc_widget.setText(
"""Duplicate entries are defined as multiple entries which point to the same file on disk. Merging these will combine the tags and metadata from all duplicates into a single consolidated entry. These are not to be confused with "duplicate files", which are duplicates of your files themselves outside of TagStudio."""
self.missing_count = QLabel()
self.missing_count.setObjectName("missingCountLabel")
self.missing_count.setStyleSheet(
# 'background:blue;'
# 'text-align:center;'
"font-weight:bold;"
"font-size:14px;"
# 'padding-top: 6px'
""
)
self.missing_count.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.missing_count.setText('Missing Files: N/A')
self.missing_count_label = QLabel()
self.missing_count_label.setObjectName("missingCountLabel")
self.missing_count_label.setStyleSheet("font-weight:bold;" "font-size:14px;")
self.missing_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.dupe_count_label = QLabel()
self.dupe_count_label.setObjectName("dupeCountLabel")
self.dupe_count_label.setStyleSheet("font-weight:bold;" "font-size:14px;")
self.dupe_count_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.refresh_unlinked_button = QPushButton()
self.refresh_unlinked_button.setText("&Refresh All")
self.refresh_unlinked_button.clicked.connect(
lambda: self.refresh_missing_files()
)
self.merge_class = MergeDuplicateEntries(self.lib, self.driver)
self.relink_class = RelinkUnlinkedEntries(self.lib, self.driver)
self.refresh_button = QPushButton()
self.refresh_button.setText("&Refresh")
self.refresh_button.clicked.connect(lambda: self.refresh_missing_files())
self.search_button = QPushButton()
self.search_button.setText("&Search && Relink")
self.relink_class.done.connect(
lambda: self.refresh_and_repair_dupe_entries(self.merge_class)
)
self.relink_class = RelinkUnlinkedEntries(self.lib, self.driver)
self.relink_class.done.connect(lambda: self.refresh_missing_files())
self.relink_class.done.connect(lambda: self.driver.update_thumbs())
self.search_button.clicked.connect(lambda: self.relink_class.repair_entries())
self.refresh_dupe_button = QPushButton()
self.refresh_dupe_button.setText("Refresh Duplicate Entries")
self.refresh_dupe_button.clicked.connect(lambda: self.refresh_dupe_entries())
self.merge_dupe_button = QPushButton()
self.merge_dupe_button.setText("&Merge Duplicate Entries")
self.merge_class.done.connect(lambda: self.set_dupe_count(-1))
self.merge_class.done.connect(lambda: self.set_missing_count(-1))
self.merge_class.done.connect(lambda: self.driver.filter_items())
self.merge_dupe_button.clicked.connect(lambda: self.merge_class.merge_entries())
self.manual_button = QPushButton()
self.manual_button.setText("&Manual Relink")
@ -107,6 +92,14 @@ class FixUnlinkedEntriesModal(QWidget):
self.delete_button.setText("De&lete Unlinked Entries")
self.delete_button.clicked.connect(lambda: self.delete_modal.show())
# self.combo_box = QComboBox()
# self.combo_box.setEditable(False)
# # self.combo_box.setMaxVisibleItems(5)
# self.combo_box.setStyleSheet('combobox-popup:0;')
# self.combo_box.view().setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
# for df in self.lib.default_fields:
# self.combo_box.addItem(f'{df["name"]} ({df["type"].replace("_", " ").title()})')
self.button_container = QWidget()
self.button_layout = QHBoxLayout(self.button_container)
self.button_layout.setContentsMargins(6, 6, 6, 6)
@ -114,39 +107,50 @@ class FixUnlinkedEntriesModal(QWidget):
self.done_button = QPushButton()
self.done_button.setText("&Done")
# self.save_button.setAutoDefault(True)
self.done_button.setDefault(True)
self.done_button.clicked.connect(self.hide)
# self.done_button.clicked.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.save_button.clicked.connect(lambda: save_callback(widget.get_content()))
self.button_layout.addWidget(self.done_button)
self.root_layout.addWidget(self.missing_count_label)
self.root_layout.addWidget(self.unlinked_desc_widget)
self.root_layout.addWidget(self.refresh_unlinked_button)
# self.returnPressed.connect(lambda: self.done.emit(self.combo_box.currentIndex()))
# self.done.connect(lambda x: callback(x))
self.root_layout.addWidget(self.desc_widget)
self.root_layout.addWidget(self.missing_count)
self.root_layout.addWidget(self.refresh_button)
self.root_layout.addWidget(self.search_button)
self.manual_button.setHidden(True)
self.root_layout.addWidget(self.manual_button)
self.root_layout.addWidget(self.delete_button)
# self.root_layout.setStretch(1,2)
self.root_layout.addStretch(1)
self.root_layout.addWidget(self.dupe_count_label)
self.root_layout.addWidget(self.dupe_desc_widget)
self.root_layout.addWidget(self.refresh_dupe_button)
self.root_layout.addWidget(self.merge_dupe_button)
self.root_layout.addStretch(2)
self.root_layout.addWidget(self.button_container)
self.set_missing_count(self.missing_count)
self.set_dupe_count(self.dupe_count)
self.set_missing_count(self.count)
def refresh_missing_files(self):
logging.info(f"Start RMF: {QThread.currentThread()}")
# pb = QProgressDialog(f'Scanning Library for Unlinked Entries...', None, 0,len(self.lib.entries))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Scanning Library')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
iterator = FunctionIterator(self.lib.refresh_missing_files)
pw = ProgressWidget(
window_title="Scanning Library",
label_text="Scanning Library for Unlinked Entries...",
label_text=f"Scanning Library for Unlinked Entries...",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries),
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v + 1))
# rmf.value.connect(lambda v: pw.update_label(f'Progress: {v}'))
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(
@ -155,76 +159,30 @@ class FixUnlinkedEntriesModal(QWidget):
pw.deleteLater(),
self.set_missing_count(len(self.lib.missing_files)),
self.delete_modal.refresh_list(),
self.refresh_dupe_entries(),
)
)
def refresh_dupe_entries(self):
iterator = FunctionIterator(self.lib.refresh_dupe_entries)
pw = ProgressWidget(
window_title="Scanning Library",
label_text="Scanning Library for Duplicate Entries...",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries),
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v + 1))
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.set_dupe_count(len(self.lib.dupe_entries)),
)
)
# r = CustomRunnable(lambda: self.lib.refresh_missing_files(lambda v: self.update_scan_value(pb, v)))
# r.done.connect(lambda: (pb.hide(), pb.deleteLater(), self.set_missing_count(len(self.lib.missing_files)), self.delete_modal.refresh_list()))
# QThreadPool.globalInstance().start(r)
# # r.run()
# pass
def refresh_and_repair_dupe_entries(self, merge_class: MergeDuplicateEntries):
iterator = FunctionIterator(self.lib.refresh_dupe_entries)
pw = ProgressWidget(
window_title="Scanning Library",
label_text="Scanning Library for Duplicate Entries...",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.entries),
)
pw.show()
iterator.value.connect(lambda v: pw.update_progress(v + 1))
r = CustomRunnable(lambda: iterator.run())
QThreadPool.globalInstance().start(r)
r.done.connect(
lambda: (
pw.hide(),
pw.deleteLater(),
self.set_dupe_count(len(self.lib.dupe_entries)),
merge_class.merge_entries(),
)
)
# def update_scan_value(self, pb:QProgressDialog, value=int):
# # pb.setLabelText(f'Scanning Library for Unlinked Entries ({value}/{len(self.lib.entries)})...')
# pb.setValue(value)
def set_missing_count(self, count: int):
self.missing_count = count
if self.missing_count < 0:
self.count = count
if self.count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count_label.setText("Unlinked Entries: N/A")
elif self.missing_count == 0:
self.missing_count.setText(f"Unlinked Entries: N/A")
elif self.count == 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count_label.setText(f"Unlinked Entries: {count}")
self.missing_count.setText(f"Unlinked Entries: {count}")
else:
self.search_button.setDisabled(False)
self.delete_button.setDisabled(False)
self.missing_count_label.setText(f"Unlinked Entries: {count}")
def set_dupe_count(self, count: int):
self.dupe_count = count
if self.dupe_count < 0:
self.dupe_count_label.setText("Duplicate Entries: N/A")
self.merge_dupe_button.setDisabled(True)
elif self.dupe_count == 0:
self.dupe_count_label.setText(f"Duplicate Entries: {count}")
self.merge_dupe_button.setDisabled(True)
else:
self.dupe_count_label.setText(f"Duplicate Entries: {count}")
self.merge_dupe_button.setDisabled(False)
self.missing_count.setText(f"Unlinked Entries: {count}")

View file

@ -1,46 +0,0 @@
# Copyright (C) 2024 Travis Abendshien (CyanVoxel).
# Licensed under the GPL-3.0 License.
# Created for TagStudio: https://github.com/CyanVoxel/TagStudio
import typing
from PySide6.QtCore import QObject, Signal, QThreadPool
from src.core.library import Library
from src.qt.helpers.function_iterator import FunctionIterator
from src.qt.helpers.custom_runnable import CustomRunnable
from src.qt.widgets.progress import ProgressWidget
# Only import for type checking/autocompletion, will not be imported at runtime.
if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
class MergeDuplicateEntries(QObject):
done = Signal()
def __init__(self, library: "Library", driver: "QtDriver"):
super().__init__()
self.lib = library
self.driver = driver
def merge_entries(self):
iterator = FunctionIterator(self.lib.merge_dupe_entries)
pw = ProgressWidget(
window_title="Merging Duplicate Entries",
label_text="",
cancel_button_text=None,
minimum=0,
maximum=len(self.lib.dupe_entries),
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x))
iterator.value.connect(
lambda: (pw.update_label("Merging Duplicate Entries..."))
)
r = CustomRunnable(lambda: iterator.run())
r.done.connect(lambda: (pw.hide(), pw.deleteLater(), self.done.emit()))
QThreadPool.globalInstance().start(r)

View file

@ -26,6 +26,20 @@ class RelinkUnlinkedEntries(QObject):
self.fixed = 0
def repair_entries(self):
# pb = QProgressDialog('', None, 0, len(self.lib.missing_files))
# # pb.setMaximum(len(self.lib.missing_files))
# pb.setFixedSize(432, 112)
# pb.setWindowFlags(pb.windowFlags() & ~Qt.WindowType.WindowCloseButtonHint)
# pb.setWindowTitle('Relinking Entries')
# pb.setWindowModality(Qt.WindowModality.ApplicationModal)
# pb.show()
# r = CustomRunnable(lambda: self.repair_entries_runnable(pb))
# r.done.connect(lambda: self.done.emit())
# # r.done.connect(lambda: self.model.clear())
# QThreadPool.globalInstance().start(r)
# # r.run()
iterator = FunctionIterator(self.lib.fix_missing_files)
pw = ProgressWidget(
@ -35,7 +49,6 @@ class RelinkUnlinkedEntries(QObject):
minimum=0,
maximum=len(self.lib.missing_files),
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0] + 1))
@ -47,6 +60,7 @@ class RelinkUnlinkedEntries(QObject):
),
)
)
# iterator.value.connect(lambda x: self.driver.purge_item_from_navigation(ItemType.ENTRY, x[1]))
r = CustomRunnable(lambda: iterator.run())
r.done.connect(
@ -59,3 +73,27 @@ class RelinkUnlinkedEntries(QObject):
def reset_fixed(self):
self.fixed = 0
# def repair_entries_runnable(self, pb: QProgressDialog):
# fixed = 0
# for i in self.lib.fix_missing_files():
# if i[1]:
# fixed += 1
# pb.setValue(i[0])
# pb.setLabelText(f'Attempting to Relink {i[0]+1}/{len(self.lib.missing_files)} Entries, {fixed} Successfully Relinked')
# for i, missing in enumerate(self.lib.missing_files):
# pb.setValue(i)
# pb.setLabelText(f'Relinking {i}/{len(self.lib.missing_files)} Unlinked Entries')
# self.lib.fix_missing_files()
# try:
# id = self.lib.get_entry_id_from_filepath(missing)
# logging.info(f'Removing Entry ID {id}:\n\t{missing}')
# self.lib.remove_entry(id)
# self.driver.purge_item_from_navigation(ItemType.ENTRY, id)
# deleted.append(missing)
# except KeyError:
# logging.info(
# f'{ERROR} \"{id}\" was reported as missing, but is not in the file_to_entry_id map.')
# for d in deleted:
# self.lib.missing_files.remove(d)

View file

@ -224,10 +224,8 @@ class QtDriver(QObject):
thread.start()
def open_library_from_dialog(self):
dir = Path(
QFileDialog.getExistingDirectory(
None, "Open/Create Library", "/", QFileDialog.ShowDirsOnly
)
dir = QFileDialog.getExistingDirectory(
None, "Open/Create Library", "/", QFileDialog.ShowDirsOnly
)
if dir not in (None, ""):
self.open_library(dir)
@ -522,7 +520,7 @@ class QtDriver(QObject):
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
QColor("#9782ff"),
)
self.open_library(Path(lib))
self.open_library(lib)
if self.args.ci:
# gracefully terminate the app in CI environment
@ -1388,7 +1386,7 @@ class QtDriver(QObject):
self.settings.endGroup()
self.settings.sync()
def open_library(self, path: Path):
def open_library(self, path):
"""Opens a TagStudio library."""
if self.lib.library_dir:
self.save_library()
@ -1397,6 +1395,14 @@ class QtDriver(QObject):
self.main_window.statusbar.showMessage(f"Opening Library {str(path)}", 3)
return_code = self.lib.open_library(path)
if return_code == 1:
# if self.args.external_preview:
# self.init_external_preview()
# if len(self.lib.entries) <= 1000:
# print(f'{INFO} Checking for missing files in Library \'{self.lib.library_dir}\'...')
# self.lib.refresh_missing_files()
# title_text = f'{self.base_title} - Library \'{self.lib.library_dir}\''
# self.main_window.setWindowTitle(title_text)
pass
else:

View file

@ -4,7 +4,6 @@
import logging
import os
from pathlib import Path
import time
import typing
from datetime import datetime as dt
@ -305,7 +304,7 @@ class PreviewPanel(QWidget):
button.setObjectName(f"path{item_key}")
def open_library_button_clicked(path):
return lambda: self.driver.open_library(Path(path))
return lambda: self.driver.open_library(path)
button.clicked.connect(open_library_button_clicked(full_val))
set_button_style(button)
@ -529,13 +528,10 @@ class PreviewPanel(QWidget):
if not image:
raise UnidentifiedImageError
except (FileNotFoundError, cv2.error) as e:
self.dimensions_label.setText(f"{extension.upper()}")
logging.info(
f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
)
except (
UnidentifiedImageError,
FileNotFoundError,
cv2.error,
DecompressionBombError,
) as e:
self.dimensions_label.setText(