From 38c0881c8e861bc9024785ef6b998dd4381c3b2c Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 20 May 2024 17:48:40 -0700 Subject: [PATCH 1/7] Reapply "Add duplicate entry handling (Fix #179)" This reverts commit 66ec0913b60a195fe3843941c4c00021bf97e023. --- tagstudio/src/core/library.py | 250 +++++++----------- tagstudio/src/qt/modals/delete_unlinked.py | 36 +-- tagstudio/src/qt/modals/fix_unlinked.py | 206 +++++++++------ tagstudio/src/qt/modals/merge_dupe_entries.py | 46 ++++ tagstudio/src/qt/modals/relink_unlinked.py | 40 +-- tagstudio/src/qt/ts_qt.py | 18 +- tagstudio/src/qt/widgets/preview_panel.py | 10 +- 7 files changed, 280 insertions(+), 326 deletions(-) create mode 100644 tagstudio/src/qt/modals/merge_dupe_entries.py diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 5d09eaa59..9fbcc2b89 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -86,7 +86,7 @@ def __repr__(self) -> str: 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) @@ -328,7 +328,7 @@ class Library: def __init__(self) -> None: # Library Info ========================================================= - self.library_dir: str = None + self.library_dir: Path = None # Entries ============================================================== # List of every Entry object. @@ -439,7 +439,7 @@ def __init__(self) -> None: {"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 @@ -447,8 +447,6 @@ def create_library(self, path) -> int: 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 path: path = path.split(TS_FOLDER_NAME)[0] @@ -468,12 +466,12 @@ def create_library(self, path) -> int: def verify_ts_folders(self) -> None: """Verifies/creates folders required by TagStudio.""" - 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_ts_path = Path() / self.library_dir / TS_FOLDER_NAME + full_backup_path = ( + Path() / self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME ) - full_collage_path = os.path.normpath( - f"{self.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}" + full_collage_path = ( + Path() / self.library_dir / TS_FOLDER_NAME / COLLAGE_FOLDER_NAME ) if not os.path.isdir(full_ts_path): @@ -522,7 +520,7 @@ def open_library(self, path: str) -> int: encoding="utf-8", ) as file: json_dump: JsonLibary = ujson.load(file) - self.library_dir = str(path) + self.library_dir = Path(path) self.verify_ts_folders() major, minor, patch = json_dump["ts-version"].split(".") @@ -678,6 +676,7 @@ def open_library(self, path: str) -> int: ) 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,19 +738,9 @@ def _map_filenames_to_entry_ids(self): """Maps a full filepath to its corresponding Entry's ID.""" self.filename_to_entry_id_map.clear() for entry in self.entries: - 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 + self.filename_to_entry_id_map[Path() / entry.path / entry.filename] = ( + entry.id + ) # def _map_filenames_to_entry_ids(self): # """Maps the file paths of entries to their index in the library list.""" @@ -901,23 +890,19 @@ def refresh_dir(self) -> Generator: # - Files without library entries # for type in TYPES: start_time = time.time() - for f in glob.glob(self.library_dir + "/**/*", recursive=True): + for f in self.library_dir.glob("**/*"): # p = Path(os.path.normpath(f)) if ( - "$RECYCLE.BIN" not in f - and TS_FOLDER_NAME not in f - and "tagstudio_thumbs" not in f - and not os.path.isdir(f) + "$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() ): - if os.path.splitext(f)[1][1:].lower() not in self.ignored_extensions: + if f.suffix not in self.ignored_extensions: self.dir_file_count += 1 - file = str(os.path.relpath(f, self.library_dir)) - + file = f.relative_to(self.library_dir) try: - if os.name == "nt": - _ = self.filename_to_entry_id_map[file.lower()] - else: - _ = self.filename_to_entry_id_map[file] + _ = self.filename_to_entry_id_map[file] except KeyError: # print(file) self.files_not_in_library.append(file) @@ -936,9 +921,7 @@ def refresh_dir(self) -> Generator: try: self.files_not_in_library = sorted( self.files_not_in_library, - key=lambda t: -os.stat( - os.path.normpath(self.library_dir + "/" + t) - ).st_ctime, + key=lambda t: -os.stat((Path() / self.library_dir / t)).st_ctime, ) except (FileExistsError, FileNotFoundError): print( @@ -969,12 +952,7 @@ def remove_entry(self, entry_id: int) -> None: # Step [1/2]: # Remove this Entry from the Entries list. entry = self.get_entry(entry_id) - path = ( - str(os.path.normpath(f"{entry.path}/{entry.filename}")) - .lstrip("\\") - .lstrip("/") - ) - path = path.lower() if os.name == "nt" else path + path = Path() / entry.path / entry.filename # logging.info(f'Removing path: {path}') del self.filename_to_entry_id_map[path] @@ -1000,51 +978,34 @@ def refresh_dupe_entries(self): `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 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("") + 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): """ @@ -1054,35 +1015,36 @@ def merge_dupe_entries(self): `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 + os.pathsep + e.filename)[0:]}..." - ) - 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): @@ -1092,9 +1054,9 @@ def refresh_dupe_files(self, results_filepath): by a DupeGuru results file. """ full_results_path = ( - os.path.normpath(f"{self.library_dir}/{results_filepath}") + Path() / self.library_dir / results_filepath if self.library_dir not in results_filepath - else os.path.normpath(f"{results_filepath}") + else Path(results_filepath) ) if os.path.exists(full_results_path): self.dupe_files.clear() @@ -1120,26 +1082,15 @@ def refresh_dupe_files(self, results_filepath): ) for match in matches: # print(f'MATCHED ({match[2]}%): \n {files[match[0]]} \n-> {files[match[1]]}') - 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])) - + 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]) + ) print("") for dupe in self.dupe_files: @@ -1215,19 +1166,16 @@ def fix_missing_files(self): 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. @@ -1255,7 +1203,7 @@ def _match_missing_file(self, file: str) -> list[str]: # matches[file].append(new_path) print( - f'[LIBRARY] MATCH: {file} \n\t-> {os.path.normpath(self.library_dir + "/" + new_path + "/" + tail)}\n' + f"[LIBRARY] MATCH: {file} \n\t-> {Path()/self.library_dir/new_path/tail}\n" ) if not matches: @@ -1344,20 +1292,12 @@ def get_entry_from_index(self, index: int) -> Entry | None: 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: - 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[ - str(os.path.normpath(os.path.relpath(filename, self.library_dir))) + Path(filename).relative_to(self.library_dir) ] except: return -1 diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index 3787b18b4..a7e2ad042 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -32,7 +32,7 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def refresh_list(self): 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( @@ -119,23 +105,3 @@ def delete_entries(self): 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() diff --git a/tagstudio/src/qt/modals/fix_unlinked.py b/tagstudio/src/qt/modals/fix_unlinked.py index 0e27f85f9..e02415b7d 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -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.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 @@ 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_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.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.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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def refresh_missing_files(self): 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(), + pw.deleteLater(), + 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) diff --git a/tagstudio/src/qt/modals/merge_dupe_entries.py b/tagstudio/src/qt/modals/merge_dupe_entries.py new file mode 100644 index 000000000..249e6d113 --- /dev/null +++ b/tagstudio/src/qt/modals/merge_dupe_entries.py @@ -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) diff --git a/tagstudio/src/qt/modals/relink_unlinked.py b/tagstudio/src/qt/modals/relink_unlinked.py index 1adffaf18..15af5cd3f 100644 --- a/tagstudio/src/qt/modals/relink_unlinked.py +++ b/tagstudio/src/qt/modals/relink_unlinked.py @@ -26,20 +26,6 @@ def __init__(self, library: "Library", driver: "QtDriver"): 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 @@ def repair_entries(self): 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 @@ def repair_entries(self): ), ) ) - # 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 @@ def increment_fixed(self): 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) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index a34e10eb5..0bcfdbf70 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -224,8 +224,10 @@ def __init__(self, core: TagStudioCore, args): 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) @@ -520,7 +522,7 @@ def start(self) -> None: 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 @@ -1386,7 +1388,7 @@ def update_libs_list(self, path: Path): 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() @@ -1395,14 +1397,6 @@ def open_library(self, path): self.main_window.statusbar.showMessage(f"Opening Library {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: diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 049115af4..3c883f9a1 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -4,6 +4,7 @@ import logging import os +from pathlib import Path import time import typing from datetime import datetime as dt @@ -304,7 +305,7 @@ def set_button_style(btn: QPushButton, extras: list[str] | None = None): 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) @@ -528,10 +529,13 @@ def update_widgets(self): 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( From ee4f8a7762be22ef32bc7acbfa36279accef1cfd Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 20 May 2024 17:50:53 -0700 Subject: [PATCH 2/7] Reapply "Fix create library + type checks" This reverts commit 57e27bb51f26793da110bab8903555a53fb82c99. --- tagstudio/src/core/library.py | 21 +++++++++++---------- tagstudio/src/qt/ts_qt.py | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 9fbcc2b89..ad9d8c2ca 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -89,17 +89,17 @@ def __eq__(self, __value: object) -> bool: # __value = cast(Self, object) if os.name == "nt": return ( - int(self.id) == int(__value.id) - and self.filename.lower() == __value.filename.lower() - and self.path.lower() == __value.path.lower() - and self.fields == __value.fields + int(self.id) == int(__value.id) #type: ignore + and self.filename.lower() == __value.filename.lower() #type: ignore + and self.path.lower() == __value.path.lower() #type: ignore + and self.fields == __value.fields #type: ignore ) else: return ( - int(self.id) == int(__value.id) - and self.filename == __value.filename - and self.path == __value.path - and self.fields == __value.fields + int(self.id) == int(__value.id) #type: ignore + and self.filename == __value.filename #type: ignore + and self.path == __value.path #type: ignore + and self.fields == __value.fields #type: ignore ) def compressed_dict(self) -> JsonEntry: @@ -448,8 +448,9 @@ def create_library(self, path: Path) -> int: """ # If '.TagStudio' is included in the path, trim the path up to it. - if TS_FOLDER_NAME in path: - path = path.split(TS_FOLDER_NAME)[0] + if TS_FOLDER_NAME in str(path): + # TODO: Native Path method instead of this casting. + path = Path(str(path).split(TS_FOLDER_NAME)[0]) try: self.clear_internal_vars() diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index 0bcfdbf70..37af0dd4f 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -1394,7 +1394,7 @@ def open_library(self, path: Path): 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: pass From 449099de8d96e90436bb8d298dd85e2403b817a2 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 20 May 2024 23:34:39 -0700 Subject: [PATCH 3/7] Type and hint changes --- tagstudio/src/core/library.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index ad9d8c2ca..c4c7a2b1f 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -86,20 +86,20 @@ def __repr__(self) -> str: 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 - and self.filename.lower() == __value.filename.lower() #type: ignore - and self.path.lower() == __value.path.lower() #type: ignore - and self.fields == __value.fields #type: ignore + int(self.id) == int(__value.id) # type: ignore + and self.filename.lower() == __value.filename.lower() # type: ignore + and self.path.lower() == __value.path.lower() # type: ignore + and self.fields == __value.fields # type: ignore ) else: return ( - int(self.id) == int(__value.id) #type: ignore - and self.filename == __value.filename #type: ignore - and self.path == __value.path #type: ignore - and self.fields == __value.fields #type: ignore + int(self.id) == int(__value.id) # type: ignore + and self.filename == __value.filename # type: ignore + and self.path == __value.path # type: ignore + and self.fields == __value.fields # type: ignore ) def compressed_dict(self) -> JsonEntry: @@ -351,7 +351,7 @@ def __init__(self) -> None: # File Interfacing ===================================================== self.dir_file_count: int = -1 - self.files_not_in_library: list[str] = [] + self.files_not_in_library: list[Path] = [] self.missing_files: list[str] = [] self.fixed_files: list[str] = [] # TODO: Get rid of this. self.missing_matches: dict = {} @@ -363,7 +363,7 @@ def __init__(self) -> None: # 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. # NOTE: On Windows, these strings are always lowercase. - self.filename_to_entry_id_map: dict[str, int] = {} + 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"] self.ignored_extensions: list = self.default_ext_blacklist @@ -500,18 +500,17 @@ def verify_default_tags(self, tag_list: list[JsonTag]) -> list[JsonTag]: return tag_list - def open_library(self, path: str) -> int: + def open_library(self, path: Path) -> int: """ Opens a TagStudio v9+ Library. Returns 0 if library does not exist, 1 if successfully opened, 2 if corrupted. """ return_code: int = 2 - path = os.path.normpath(path).rstrip("\\") # If '.TagStudio' is included in the path, trim the path up to it. - if TS_FOLDER_NAME in path: - path = path.split(TS_FOLDER_NAME)[0] + if TS_FOLDER_NAME in str(path): + path = Path(str(path).split(TS_FOLDER_NAME)[0]) if os.path.exists(os.path.normpath(f"{path}/{TS_FOLDER_NAME}/ts_library.json")): try: From 4217dd08ccd6e49965e02e14fcd1bc24c6bef508 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Mon, 20 May 2024 23:45:03 -0700 Subject: [PATCH 4/7] Remove object cast --- tagstudio/src/core/library.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index c4c7a2b1f..ddf2b1114 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -86,7 +86,7 @@ def __repr__(self) -> str: 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 From 298c21d112181b3017d2fa62535b9de444b164f8 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Tue, 21 May 2024 00:11:27 -0700 Subject: [PATCH 5/7] MyPy wrestling --- tagstudio/src/qt/modals/fix_unlinked.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tagstudio/src/qt/modals/fix_unlinked.py b/tagstudio/src/qt/modals/fix_unlinked.py index e02415b7d..72d115cda 100644 --- a/tagstudio/src/qt/modals/fix_unlinked.py +++ b/tagstudio/src/qt/modals/fix_unlinked.py @@ -195,8 +195,8 @@ def refresh_and_repair_dupe_entries(self, merge_class: MergeDuplicateEntries): QThreadPool.globalInstance().start(r) r.done.connect( lambda: ( - pw.hide(), - pw.deleteLater(), + pw.hide(), # type: ignore + pw.deleteLater(), # type: ignore self.set_dupe_count(len(self.lib.dupe_entries)), merge_class.merge_entries(), ) From f6a7a2f928b35966e754dbde0b55e31d527d265c Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Thu, 23 May 2024 13:01:24 -0700 Subject: [PATCH 6/7] 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 --- tagstudio/src/core/library.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index ddf2b1114..b91e70f37 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -86,20 +86,20 @@ def __repr__(self) -> str: return self.__str__() def __eq__(self, __value: object) -> bool: - # __value = cast(Self, object) + __value = cast(Self, __value) if os.name == "nt": return ( - int(self.id) == int(__value.id) # type: ignore - and self.filename.lower() == __value.filename.lower() # type: ignore - and self.path.lower() == __value.path.lower() # type: ignore - and self.fields == __value.fields # type: ignore + int(self.id) == int(__value.id) + and self.filename.lower() == __value.filename.lower() + and self.path.lower() == __value.path.lower() + and self.fields == __value.fields ) else: return ( - int(self.id) == int(__value.id) # type: ignore - and self.filename == __value.filename # type: ignore - and self.path == __value.path # type: ignore - and self.fields == __value.fields # type: ignore + int(self.id) == int(__value.id) + and self.filename == __value.filename + and self.path == __value.path + and self.fields == __value.fields ) def compressed_dict(self) -> JsonEntry: From 082cac2497ec375fca5f5e5c8073142aadf606e6 Mon Sep 17 00:00:00 2001 From: Travis Abendshien Date: Wed, 29 May 2024 15:44:53 -0700 Subject: [PATCH 7/7] Fix formatting + mypy --- tagstudio/src/core/library.py | 23 ++++++++--------------- tagstudio/src/qt/resources_rc.py | 12 ++++++------ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 4af62a0d6..7bd26d116 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -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 @@ -90,13 +85,12 @@ def __repr__(self) -> str: def __eq__(self, __value: object) -> bool: __value = cast(Self, __value) - if os.name == "nt": - return ( - int(self.id) == int(__value.id) - and self.filename == __value.filename - and self.path == __value.path - and self.fields == __value.fields - ) + return ( + int(self.id) == int(__value.id) + and self.filename == __value.filename + and self.path == __value.path + and self.fields == __value.fields + ) def compressed_dict(self) -> JsonEntry: """ @@ -1146,7 +1140,6 @@ def fix_missing_files(self): # TODO - the type here doesnt match but I cant reproduce calling this self.remove_missing_matches(fixed_indices) - def _match_missing_file(self, file: str) -> list[Path]: """ Tries to find missing entry files within the library directory. diff --git a/tagstudio/src/qt/resources_rc.py b/tagstudio/src/qt/resources_rc.py index 518bd2ec7..f11bf7fb9 100644 --- a/tagstudio/src/qt/resources_rc.py +++ b/tagstudio/src/qt/resources_rc.py @@ -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 @@ \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():