Duplicate Entry Handling (Fixes #179) (#204)

* Reapply "Add duplicate entry handling (Fix #179)"

This reverts commit 66ec0913b6.

* Reapply "Fix create library + type checks"

This reverts commit 57e27bb51f.

* Type and hint changes

* Remove object cast

* MyPy wrestling

* Remove type: ignore, change __eq__ cast

- Remove `type: ignore` comments from `Entry`'s `__eq__` method
- Change the cast in this method from `__value = cast(Self, object)` to `__value = cast(Self, __value)`

Co-Authored-By: Jiri <yedpodtrzitko@users.noreply.github.com>

* Fix formatting + mypy

---------

Co-authored-by: Jiri <yedpodtrzitko@users.noreply.github.com>
This commit is contained in:
Travis Abendshien
2024-05-29 15:47:37 -07:00
committed by GitHub
parent 9f630fe315
commit 868b553670
8 changed files with 256 additions and 257 deletions

View File

@@ -5,23 +5,18 @@
"""The Library object and related methods for TagStudio."""
import datetime
import glob
import logging
import os
import sys
import time
import traceback
import typing
import xml.etree.ElementTree as ET
import ujson
from enum import Enum
from pathlib import Path
from typing import cast, Generator
from typing_extensions import Self
import ujson
from pathlib import Path
from src.core.json_typing import JsonCollation, JsonEntry, JsonLibary, JsonTag
from src.core.utils.str import strip_punctuation
from src.core.utils.web import strip_web_protocol
@@ -89,7 +84,7 @@ class Entry:
return self.__str__()
def __eq__(self, __value: object) -> bool:
__value = cast(Self, object)
__value = cast(Self, __value)
return (
int(self.id) == int(__value.id)
and self.filename == __value.filename
@@ -343,7 +338,6 @@ class Library:
# Maps the filenames of entries in the Library to their entry's index in the self.entries list.
# Used for O(1) lookup of a file based on the current index (page number - 1) of the image being looked at.
# That filename can then be used to provide quick lookup to image metadata entries in the Library.
self.filename_to_entry_id_map: dict[Path, int] = {}
# A list of file extensions to be ignored by TagStudio.
self.default_ext_blacklist: list = ["json", "xmp", "aae"]
@@ -420,7 +414,7 @@ class Library:
{"id": 30, "name": "Comments", "type": "text_box"},
]
def create_library(self, path) -> int:
def create_library(self, path: Path) -> int:
"""
Creates a TagStudio library in the given directory.\n
Return Codes:\n
@@ -657,6 +651,7 @@ 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"
@@ -946,42 +941,34 @@ 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()
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 (
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("")
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)
def merge_dupe_entries(self):
"""
@@ -991,35 +978,36 @@ class Library:
`dupe_entries = tuple(int, list[int])`
"""
print("[LIBRARY] Mirroring Duplicate Entries...")
logging.info("[LIBRARY] Mirroring Duplicate Entries...")
id_to_entry_map: dict = {}
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])
# 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(
logging.info(
"[LIBRARY] Consolidating Entries... (This may take a while for larger libraries)"
)
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 / e.filename}..."
)
print("")
# [unique.append(x) for x in self.entries if x not in unique]
self.entries = unique
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)
self._map_filenames_to_entry_ids()
def refresh_dupe_files(self, results_filepath: str | Path):
@@ -1028,6 +1016,7 @@ class Library:
A duplicate file is defined as an identical or near-identical file as determined
by a DupeGuru results file.
"""
full_results_path: Path = Path(results_filepath)
if self.library_dir not in full_results_path.parents:
full_results_path = self.library_dir / full_results_path
@@ -1066,8 +1055,6 @@ class Library:
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:
@@ -1143,19 +1130,16 @@ 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[Path]:
"""
Tries to find missing entry files within the library directory.
@@ -1295,7 +1279,7 @@ class Library:
return None
# @deprecated('Use new Entry ID system.')
def get_entry_id_from_filepath(self, filename):
def get_entry_id_from_filepath(self, filename: Path):
"""Returns an Entry ID given the full filepath it points to."""
try:
if self.entries:

View File

@@ -32,7 +32,7 @@ class DeleteUnlinkedEntriesModal(QWidget):
super().__init__()
self.lib = library
self.driver = driver
self.setWindowTitle(f"Delete Unlinked Entries")
self.setWindowTitle("Delete Unlinked Entries")
self.setWindowModality(Qt.WindowModality.ApplicationModal)
self.setMinimumSize(500, 400)
self.root_layout = QVBoxLayout(self)
@@ -81,20 +81,6 @@ class DeleteUnlinkedEntriesModal(QWidget):
self.model.appendRow(QStandardItem(str(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(
@@ -119,23 +105,3 @@ 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 QThread, Qt, QThreadPool
from PySide6.QtCore import Qt, QThreadPool
from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton
from src.core.library import Library
@@ -14,6 +14,7 @@ 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.
@@ -21,65 +22,79 @@ if typing.TYPE_CHECKING:
from src.qt.ts_qt import QtDriver
ERROR = f"[ERROR]"
WARNING = f"[WARNING]"
INFO = f"[INFO]"
ERROR = "[ERROR]"
WARNING = "[WARNING]"
INFO = "[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.count = -1
self.setWindowTitle(f"Fix Unlinked Entries")
self.missing_count = -1
self.dupe_count = -1
self.setWindowTitle("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.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.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.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.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.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.setAlignment(Qt.AlignmentFlag.AlignCenter)
# self.missing_count.setText('Missing Files: N/A')
self.refresh_button = QPushButton()
self.refresh_button.setText("&Refresh")
self.refresh_button.clicked.connect(lambda: self.refresh_missing_files())
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.search_button = QPushButton()
self.search_button.setText("&Search && Relink")
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.relink_class.done.connect(
lambda: self.refresh_and_repair_dupe_entries(self.merge_class)
)
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")
@@ -92,14 +107,6 @@ 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)
@@ -107,50 +114,39 @@ 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.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.missing_count_label)
self.root_layout.addWidget(self.unlinked_desc_widget)
self.root_layout.addWidget(self.refresh_unlinked_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.count)
self.set_missing_count(self.missing_count)
self.set_dupe_count(self.dupe_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=f"Scanning Library for Unlinked Entries...",
label_text="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(
@@ -159,30 +155,76 @@ class FixUnlinkedEntriesModal(QWidget):
pw.deleteLater(),
self.set_missing_count(len(self.lib.missing_files)),
self.delete_modal.refresh_list(),
self.refresh_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_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)),
)
)
# 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 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(), # type: ignore
pw.deleteLater(), # type: ignore
self.set_dupe_count(len(self.lib.dupe_entries)),
merge_class.merge_entries(),
)
)
def set_missing_count(self, count: int):
self.count = count
if self.count < 0:
self.missing_count = count
if self.missing_count < 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f"Unlinked Entries: N/A")
elif self.count == 0:
self.missing_count_label.setText("Unlinked Entries: N/A")
elif self.missing_count == 0:
self.search_button.setDisabled(True)
self.delete_button.setDisabled(True)
self.missing_count.setText(f"Unlinked Entries: {count}")
self.missing_count_label.setText(f"Unlinked Entries: {count}")
else:
self.search_button.setDisabled(False)
self.delete_button.setDisabled(False)
self.missing_count.setText(f"Unlinked Entries: {count}")
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)

View File

@@ -0,0 +1,46 @@
# 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,20 +26,6 @@ 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(
@@ -49,6 +35,7 @@ class RelinkUnlinkedEntries(QObject):
minimum=0,
maximum=len(self.lib.missing_files),
)
pw.show()
iterator.value.connect(lambda x: pw.update_progress(x[0] + 1))
@@ -60,7 +47,6 @@ 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(
@@ -73,27 +59,3 @@ 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

@@ -1,6 +1,6 @@
# Resource object code (Python 3)
# Created by: object code
# Created by: The Resource Compiler for Qt version 6.5.1
# Created by: The Resource Compiler for Qt version 6.6.3
# WARNING! All changes made in this file will be lost!
from PySide6 import QtCore
@@ -16232,15 +16232,15 @@ qt_resource_struct = b"\
\x00\x00\x00\x00\x00\x02\x00\x00\x00\x05\x00\x00\x00\x02\
\x00\x00\x00\x00\x00\x00\x00\x00\
\x00\x00\x00\x96\x00\x00\x00\x00\x00\x01\x00\x03\xca\xbe\
\x00\x00\x01\x8f\x10b\x06\xcd\
\x00\x00\x01\x8a\xfb\xb4\xd6\xbe\
\x00\x00\x00b\x00\x00\x00\x00\x00\x01\x00\x03\xb8\x1a\
\x00\x00\x01\x8f\x10b\x06\xca\
\x00\x00\x01\x8a\xfb\xc6t\x9f\
\x00\x00\x00\xca\x00\x00\x00\x00\x00\x01\x00\x03\xe4\x99\
\x00\x00\x01\x8f\x10b\x06\xc8\
\x00\x00\x01\x8a\xfb\xb4\xc1\x95\
\x00\x00\x00\x12\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\
\x00\x00\x01\x8f\x10b\x06\xce\
\x00\x00\x01\x8a\xfb\xc6\x86\xda\
\x00\x00\x00H\x00\x00\x00\x00\x00\x01\x00\x00\x22\x83\
\x00\x00\x01\x8f\x10b\x06\xcc\
\x00\x00\x01\x8e\xfd%\xc3\xc7\
"
def qInitResources():

View File

@@ -223,8 +223,10 @@ class QtDriver(QObject):
thread.start()
def open_library_from_dialog(self):
dir = QFileDialog.getExistingDirectory(
None, "Open/Create Library", "/", QFileDialog.ShowDirsOnly
dir = Path(
QFileDialog.getExistingDirectory(
None, "Open/Create Library", "/", QFileDialog.ShowDirsOnly
)
)
if dir not in (None, ""):
self.open_library(dir)
@@ -529,7 +531,7 @@ class QtDriver(QObject):
int(Qt.AlignmentFlag.AlignBottom | Qt.AlignmentFlag.AlignHCenter),
QColor("#9782ff"),
)
self.open_library(lib)
self.open_library(Path(lib))
if self.args.ci:
# gracefully terminate the app in CI environment
@@ -1400,23 +1402,15 @@ class QtDriver(QObject):
self.settings.endGroup()
self.settings.sync()
def open_library(self, path):
def open_library(self, path: Path):
"""Opens a TagStudio library."""
if self.lib.library_dir:
self.save_library()
self.lib.clear_internal_vars()
self.main_window.statusbar.showMessage(f"Opening Library {path}", 3)
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,6 +4,7 @@
import logging
import os
from pathlib import Path
import time
import typing
from datetime import datetime as dt
@@ -308,7 +309,7 @@ class PreviewPanel(QWidget):
button.setObjectName(f"path{item_key}")
def open_library_button_clicked(path):
return lambda: self.driver.open_library(path)
return lambda: self.driver.open_library(Path(path))
button.clicked.connect(open_library_button_clicked(full_val))
set_button_style(button)
@@ -561,9 +562,13 @@ class PreviewPanel(QWidget):
f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})"
)
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,
cv2.error,
DecompressionBombError,
) as e:
self.dimensions_label.setText(