diff --git a/tagstudio/src/cli/ts_cli.py b/tagstudio/src/cli/ts_cli.py index c5eb0b88d..fba30860d 100644 --- a/tagstudio/src/cli/ts_cli.py +++ b/tagstudio/src/cli/ts_cli.py @@ -88,21 +88,19 @@ def __init__(self, core, args): self.is_dupe_file_count_init: bool = False self.external_preview_size: tuple[int, int] = (960, 960) - epd_path = os.path.normpath( - f"{Path(__file__).parents[2]}/resources/cli/images/external_preview.png" + epd_path = ( + Path(__file__).parents[2] / "resources/cli/images/external_preview.png" ) self.external_preview_default: Image = ( Image.open(epd_path) - if os.path.exists(epd_path) + if epd_path.exists() else Image.new(mode="RGB", size=(self.external_preview_size)) ) self.external_preview_default.thumbnail(self.external_preview_size) - epb_path = os.path.normpath( - f"{Path(__file__).parents[2]}/resources/cli/images/no_preview.png" - ) + epb_path = Path(__file__).parents[3] / "resources/cli/images/no_preview.png" self.external_preview_broken: Image = ( Image.open(epb_path) - if os.path.exists(epb_path) + if epb_path.exists() else Image.new(mode="RGB", size=(self.external_preview_size)) ) self.external_preview_broken.thumbnail(self.external_preview_size) @@ -361,45 +359,44 @@ def paste_field_from_buffer(self, entry_id) -> None: def init_external_preview(self) -> None: """Initialized the external preview image file.""" if self.lib and self.lib.library_dir: - external_preview_path: str = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/external_preview.jpg" + external_preview_path: Path = ( + self.lib.library_dir / TS_FOLDER_NAME / "external_preview.jpg" ) - if not os.path.isfile(external_preview_path): + if not external_preview_path.is_file(): temp = self.external_preview_default temp.save(external_preview_path) - open_file(external_preview_path) def set_external_preview_default(self) -> None: """Sets the external preview to its default image.""" if self.lib and self.lib.library_dir: - external_preview_path: str = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/external_preview.jpg" + external_preview_path: Path = ( + self.lib.library_dir / TS_FOLDER_NAME / "external_preview.jpg" ) - if os.path.isfile(external_preview_path): + if external_preview_path.is_file(): temp = self.external_preview_default temp.save(external_preview_path) def set_external_preview_broken(self) -> None: """Sets the external preview image file to the 'broken' placeholder.""" if self.lib and self.lib.library_dir: - external_preview_path: str = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/external_preview.jpg" + external_preview_path: Path = ( + self.lib.library_dir / TS_FOLDER_NAME / "external_preview.jpg" ) - if os.path.isfile(external_preview_path): + if external_preview_path.is_file(): temp = self.external_preview_broken temp.save(external_preview_path) def close_external_preview(self) -> None: """Destroys and closes the external preview image file.""" if self.lib and self.lib.library_dir: - external_preview_path: str = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/external_preview.jpg" + external_preview_path: Path = ( + self.lib.library_dir / TS_FOLDER_NAME / "external_preview.jpg" ) - if os.path.isfile(external_preview_path): + if external_preview_path.is_file(): os.remove(external_preview_path) - def scr_create_library(self, path=""): + def scr_create_library(self, path=None): """Screen for creating a new TagStudio library.""" subtitle = "Create Library" @@ -412,7 +409,10 @@ def scr_create_library(self, path=""): if not path: print("Enter Library Folder Path: \n> ", end="") path = input() - if os.path.exists(path): + + path = Path(path) + + if path.exists(): print("") print( f'{INFO} Are you sure you want to create a new Library at "{path}"? (Y/N)\n> ', @@ -488,9 +488,7 @@ def backup_library(self, display_message: bool = True) -> bool: """Saves a backup copy of the Library file to disk. Returns True if successful.""" if self.lib and self.lib.library_dir: filename = self.lib.save_library_backup_to_disk() - location = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/backups/{filename}" - ) + location = self.lib.library_dir / TS_FOLDER_NAME / "backups" / filename if display_message: print(f'{INFO} Backup of Library saved at "{location}".') return True @@ -617,13 +615,11 @@ def print_thumbnail( """ entry = None if index < 0 else self.lib.entries[index] if entry: - filepath = os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) - external_preview_path: str = "" + filepath = self.lib.library_dir / entry.path / entry.filename + external_preview_path: Path = None if self.args.external_preview: - external_preview_path = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/external_preview.jpg" + external_preview_path = ( + self.lib.library_dir / TS_FOLDER_NAME / "external_preview.jpg" ) # thumb_width = min( # os.get_terminal_size()[0]//2, @@ -674,14 +670,9 @@ def print_thumbnail( final_frame = Image.fromarray(frame) w, h = final_frame.size final_frame.save( - os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/temp.jpg" - ), - quality=50, - ) - final_img_path = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/temp.jpg" + self.lib.library_dir / TS_FOLDER_NAME / "temp.jpg", quality=50 ) + final_img_path = self.lib.library_dir / TS_FOLDER_NAME / "temp.jpg" # NOTE: Temporary way to hack a non-terminal preview. if self.args.external_preview and entry: final_frame.thumbnail(self.external_preview_size) @@ -744,7 +735,7 @@ def print_thumbnail( print(image.replace("\n", ("\n" + " " * spacing))) if file_type in VIDEO_TYPES: - os.remove(f"{self.lib.library_dir}/{TS_FOLDER_NAME}/temp.jpg") + os.remove(self.lib.library_dir / TS_FOLDER_NAME / "temp.jpg") except: if not self.args.external_preview or not entry: print( @@ -901,7 +892,7 @@ def run_macro(self, name: str, entry_id: int): """Runs a specific Macro on an Entry given a Macro name.""" # entry: Entry = self.lib.get_entry_from_index(entry_id) entry = self.lib.get_entry(entry_id) - path = os.path.normpath(f"{self.lib.library_dir}/{entry.path}/{entry.filename}") + path = self.lib.library_dir / entry.path / entry.filename source = path.split(os.sep)[1].lower() if name == "sidecar": self.lib.add_generic_data_to_entry( @@ -1054,8 +1045,11 @@ def create_collage(self) -> str: time.sleep(5) collage = Image.new("RGB", (img_size, img_size)) - filename = os.path.normpath( - f'{self.lib.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}/collage_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.png' + filename = ( + elf.lib.library_dir + / TS_FOLDER_NAME + / COLLAGE_FOLDER_NAME + / f'collage_{datetime.datetime.utcnow().strftime("%F_%T").replace(":", "")}.png' ) i = 0 @@ -1065,10 +1059,7 @@ def create_collage(self) -> str: if i < len(self.lib.entries) and run: # entry: Entry = self.lib.get_entry_from_index(i) entry = self.lib.entries[i] - filepath = os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) - file_type = os.path.splitext(filepath)[1].lower()[1:] + filepath = self.lib.library_dir / entry.path / entry.filename color: str = "" if data_tint_mode or data_only_mode: @@ -1113,16 +1104,17 @@ def create_collage(self) -> str: collage.paste(pic, (y * thumb_size, x * thumb_size)) if not data_only_mode: print( - f"\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}" + f"\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(filepath.suffix.lower())}{entry.path}{os.sep}{entry.filename}{RESET}" ) # sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}') # sys.stdout.flush() - if file_type in IMAGE_TYPES: + + if filepath.suffix.lower() in IMAGE_TYPES: try: with Image.open( - os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) + self.lib.library_dir + / entry.path + / entry.filename ) as pic: if keep_aspect: pic.thumbnail((thumb_size, thumb_size)) @@ -1146,7 +1138,7 @@ def create_collage(self) -> str: f"[ERROR] One of the images was too big ({e})" ) - elif file_type in VIDEO_TYPES: + elif filepath.suffix.lower() in VIDEO_TYPES: video = cv2.VideoCapture(filepath) video.set( cv2.CAP_PROP_POS_FRAMES, @@ -1168,9 +1160,7 @@ def create_collage(self) -> str: ) collage.paste(pic, (y * thumb_size, x * thumb_size)) except UnidentifiedImageError: - print( - f"\n{ERROR} Couldn't read {entry.path}{os.sep}{entry.filename}" - ) + print(f"\n{ERROR} Couldn't read {entry.path / entry.filename}") except KeyboardInterrupt: # self.quit(save=False, backup=True) run = False @@ -1178,7 +1168,7 @@ def create_collage(self) -> str: print(f"{INFO} Collage operation cancelled.") clear_scr = False except: - print(f"{ERROR} {entry.path}{os.sep}{entry.filename}") + print(f"{ERROR} {entry.path / entry.filename}") traceback.print_exc() print("Continuing...") i = i + 1 @@ -1455,7 +1445,7 @@ def scr_library_home(self, clear_scr=True): f"{WHITE_FG}Enter the filename for your DupeGuru results file:\n> {RESET}", end="", ) - dg_results_file = os.path.normpath(input()) + dg_results_file = Path(input()) print( f"{INFO} Checking for duplicate files in Library '{self.lib.library_dir}'..." ) @@ -1477,7 +1467,7 @@ def scr_library_home(self, clear_scr=True): ) > 1: if com[1].lower() == "entries": for i, e in enumerate(self.lib.entries, start=0): - title = f"[{i+1}/{len(self.lib.entries)}] {self.lib.entries[i].path}{os.path.sep}{self.lib.entries[i].filename}" + title = f"[{i+1}/{len(self.lib.entries)}] {self.lib.entries[i].path / os.path.sep / self.lib.entries[i].filename}" print( self.format_subtitle( title, @@ -1526,12 +1516,11 @@ def scr_library_home(self, clear_scr=True): for dupe in self.lib.dupe_entries: print( self.lib.entries[dupe[0]].path - + os.path.sep - + self.lib.entries[dupe[0]].filename + / self.lib.entries[dupe[0]].filename ) for d in dupe[1]: print( - f"\t-> {(self.lib.entries[d].path + os.path.sep + self.lib.entries[d].filename)}" + f"\t-> {(self.lib.entries[d].path / self.lib.entries[d].filename)}" ) time.sleep(0.1) print("Press Enter to Continue...") @@ -1871,20 +1860,18 @@ def scr_browse_entries_gallery(self, index, clear_scr=True, refresh=True): # entry = self.lib.get_entry_from_index( # self.filtered_entries[index]) entry = self.lib.get_entry(self.filtered_entries[index][1]) - filename = f'{os.path.normpath(self.lib.library_dir + "/" + entry.path + "/" + entry.filename)}' + filename = self.lib.library_dir / entry.path / entry.filename # if self.lib.is_legacy_library: # title += ' (Legacy Format)' h1 = f"[{index + 1}/{len(self.filtered_entries)}] {filename}" # print(self.format_subtitle(subtitle)) print( - self.format_h1( - h1, self.get_file_color(os.path.splitext(filename)[1]) - ) + self.format_h1(h1, self.get_file_color(filename.suffix.lower())) ) print("") - if not os.path.isfile(filename): + if not filename.is_file(): print( f"{RED_BG}{BRIGHT_WHITE_FG}[File Missing]{RESET}{BRIGHT_RED_FG} (Run 'fix missing' to resolve){RESET}" ) @@ -2527,7 +2514,7 @@ def scr_choose_missing_match(self, index, clear_scr=True, refresh=True) -> int: while True: entry = self.lib.get_entry_from_index(index) - filename = f'{os.path.normpath(self.lib.library_dir + "/" + entry.path + "/" + entry.filename)}' + filename = self.lib.library_dir / entry.path / entry.filename if refresh: if clear_scr: @@ -2542,7 +2529,7 @@ def scr_choose_missing_match(self, index, clear_scr=True, refresh=True) -> int: for i, match in enumerate(self.lib.missing_matches[filename]): print(self.format_h1(f"[{i+1}] {match}"), end="\n\n") - fn = f'{os.path.normpath(self.lib.library_dir + "/" + match + "/" + entry.filename)}' + fn = self.lib.library_dir / match / entry.filename self.print_thumbnail( index=-1, filepath=fn, @@ -2585,9 +2572,7 @@ def scr_choose_missing_match(self, index, clear_scr=True, refresh=True) -> int: # Open ============================================================= elif com[0].lower() == "open" or com[0].lower() == "o": for match in self.lib.missing_matches[filename]: - fn = os.path.normpath( - self.lib.library_dir + "/" + match + "/" + entry.filename - ) + fn = self.lib.library_dir / match / entry.filename open_file(fn) refresh = False # clear() @@ -2630,9 +2615,7 @@ def scr_resolve_dupe_files(self, index, clear_scr=True): while True: dupe = self.lib.dupe_files[index] - if os.path.exists(os.path.normpath(f"{dupe[0]}")) and os.path.exists( - os.path.normpath(f"{dupe[1]}") - ): + if dupe[0].exists() and dupe[1].exists(): # entry = self.lib.get_entry_from_index(index_1) entry_1_index = self.lib.get_entry_id_from_filepath(dupe[0]) entry_2_index = self.lib.get_entry_id_from_filepath(dupe[1]) @@ -2779,7 +2762,7 @@ def scr_edit_entry_tag_box(self, entry_index, field_index, clear_scr=True): title = f"{self.base_title} - Library '{self.lib.library_dir}'" entry = self.lib.entries[entry_index] - filename = f'{os.path.normpath(self.lib.library_dir + "/" + entry.path + "/" + entry.filename)}' + filename = self.lib.library_dir / entry.path / entry.filename field_name = self.lib.get_field_attr(entry.fields[field_index], "name") subtitle = f'Editing "{field_name}" Field' h1 = f"{filename}" @@ -2796,7 +2779,7 @@ def scr_edit_entry_tag_box(self, entry_index, field_index, clear_scr=True): ) print("") - if not os.path.isfile(filename): + if not filename.is_file(): print( f"{RED_BG}{BRIGHT_WHITE_FG}[File Missing]{RESET}{BRIGHT_RED_FG} (Run 'fix missing' to resolve){RESET}" ) @@ -3048,7 +3031,7 @@ def scr_edit_entry_text( title = f"{self.base_title} - Library '{self.lib.library_dir}'" entry = self.lib.entries[entry_index] - filename = f'{os.path.normpath(self.lib.library_dir + "/" + entry.path + "/" + entry.filename)}' + filename = self.lib.library_dir / entry.path / entry.filename field_name = self.lib.get_field_attr(entry.fields[field_index], "name") subtitle = f'Editing "{field_name}" Field' h1 = f"{filename}" @@ -3061,7 +3044,7 @@ def scr_edit_entry_text( print(self.format_h1(h1, self.get_file_color(os.path.splitext(filename)[1]))) print("") - if not os.path.isfile(filename): + if not filename.is_file(): print( f"{RED_BG}{BRIGHT_WHITE_FG}[File Missing]{RESET}{BRIGHT_RED_FG} (Run 'fix missing' to resolve){RESET}" ) diff --git a/tagstudio/src/core/constants.py b/tagstudio/src/core/constants.py index 8ae3cd1aa..6cf585f05 100644 --- a/tagstudio/src/core/constants.py +++ b/tagstudio/src/core/constants.py @@ -9,76 +9,86 @@ # TODO: Turn this whitelist into a user-configurable blacklist. IMAGE_TYPES: list[str] = [ - "png", - "jpg", - "jpeg", - "jpg_large", - "jpeg_large", - "jfif", - "gif", - "tif", - "tiff", - "heic", - "heif", - "webp", - "bmp", - "svg", - "avif", - "apng", - "jp2", - "j2k", - "jpg2", + ".png", + ".jpg", + ".jpeg", + ".jpg_large", + ".jpeg_large", + ".jfif", + ".gif", + ".tif", + ".tiff", + ".heic", + ".heif", + ".webp", + ".bmp", + ".svg", + ".avif", + ".apng", + ".jp2", + ".j2k", + ".jpg2", ] -RAW_IMAGE_TYPES: list[str] = ["raw", "dng", "rw2", "nef", "arw", "crw", "cr3"] +RAW_IMAGE_TYPES: list[str] = [".raw", ".dng", ".rw2", ".nef", ".arw", ".crw", ".cr3"] VIDEO_TYPES: list[str] = [ - "mp4", - "webm", - "mov", - "hevc", - "mkv", - "avi", - "wmv", - "flv", - "gifv", - "m4p", - "m4v", - "3gp", + ".mp4", + ".webm", + ".mov", + ".hevc", + ".mkv", + ".avi", + ".wmv", + ".flv", + ".gifv", + ".m4p", + ".m4v", + ".3gp", ] AUDIO_TYPES: list[str] = [ - "mp3", - "mp4", - "mpeg4", - "m4a", - "aac", - "wav", - "flac", - "alac", - "wma", - "ogg", - "aiff", + ".mp3", + ".mp4", + ".mpeg4", + ".m4a", + ".aac", + ".wav", + ".flac", + ".alac", + ".wma", + ".ogg", + ".aiff", +] +DOC_TYPES: list[str] = [ + ".txt", + ".rtf", + ".md", + ".doc", + ".docx", + ".pdf", + ".tex", + ".odt", + ".pages", ] -DOC_TYPES: list[str] = ["txt", "rtf", "md", "doc", "docx", "pdf", "tex", "odt", "pages"] PLAINTEXT_TYPES: list[str] = [ - "txt", - "md", - "css", - "html", - "xml", - "json", - "js", - "ts", - "ini", - "htm", - "csv", - "php", - "sh", - "bat", + ".txt", + ".md", + ".css", + ".html", + ".xml", + ".json", + ".js", + ".ts", + ".ini", + ".htm", + ".csv", + ".php", + ".sh", + ".bat", ] -SPREADSHEET_TYPES: list[str] = ["csv", "xls", "xlsx", "numbers", "ods"] -PRESENTATION_TYPES: list[str] = ["ppt", "pptx", "key", "odp"] -ARCHIVE_TYPES: list[str] = ["zip", "rar", "tar", "tar.gz", "tgz", "7z"] -PROGRAM_TYPES: list[str] = ["exe", "app"] -SHORTCUT_TYPES: list[str] = ["lnk", "desktop", "url"] +SPREADSHEET_TYPES: list[str] = [".csv", ".xls", ".xlsx", ".numbers", ".ods"] +PRESENTATION_TYPES: list[str] = [".ppt", ".pptx", ".key", ".odp"] +ARCHIVE_TYPES: list[str] = [".zip", ".rar", ".tar", ".tar", ".gz", ".tgz", ".7z"] +PROGRAM_TYPES: list[str] = [".exe", ".app"] +SHORTCUT_TYPES: list[str] = [".lnk", ".desktop", ".url"] ALL_FILE_TYPES: list[str] = ( IMAGE_TYPES diff --git a/tagstudio/src/core/library.py b/tagstudio/src/core/library.py index 5d09eaa59..4d2af6846 100644 --- a/tagstudio/src/core/library.py +++ b/tagstudio/src/core/library.py @@ -20,6 +20,7 @@ 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 @@ -48,11 +49,13 @@ class ItemType(Enum): class Entry: """A Library Entry Object. Referenced by ID.""" - def __init__(self, id: int, filename: str, path: str, fields: list[dict]) -> None: + def __init__( + self, id: int, filename: str | Path, path: str | Path, fields: list[dict] + ) -> None: # Required Fields ====================================================== self.id = int(id) - self.filename = filename - self.path = path + self.filename = Path(filename) + self.path = Path(path) self.fields: list[dict] = fields self.type = None @@ -87,20 +90,12 @@ def __repr__(self) -> str: 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 - ) - else: - 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: """ @@ -109,9 +104,9 @@ def compressed_dict(self) -> JsonEntry: """ obj: JsonEntry = {"id": self.id} if self.filename: - obj["filename"] = self.filename + obj["filename"] = str(self.filename) if self.path: - obj["path"] = self.path + obj["path"] = str(self.path) if self.fields: obj["fields"] = self.fields @@ -285,23 +280,9 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() - @typing.no_type_check def __eq__(self, __value: object) -> bool: __value = cast(Self, __value) - 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 - ) - else: - 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.fields == __value.fields def compressed_dict(self) -> JsonCollation: """ @@ -328,7 +309,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. @@ -351,19 +332,19 @@ def __init__(self) -> None: # File Interfacing ===================================================== self.dir_file_count: int = -1 - self.files_not_in_library: list[str] = [] - self.missing_files: list[str] = [] - self.fixed_files: list[str] = [] # TODO: Get rid of this. + self.files_not_in_library: list[Path] = [] + self.missing_files: list[Path] = [] + self.fixed_files: list[Path] = [] # TODO: Get rid of this. self.missing_matches: dict = {} # Duplicate Files # Defined by files that are exact or similar copies to others. Generated by DupeGuru. # (Filepath, Matched Filepath, Match Percentage) - self.dupe_files: list[tuple[str, str, int]] = [] + self.dupe_files: list[tuple[Path, Path, int]] = [] # 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. - # 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 @@ -447,15 +428,11 @@ 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] + path = self._fix_lib_path(path) try: self.clear_internal_vars() - self.library_dir = path + self.library_dir = Path(path) self.verify_ts_folders() self.save_library_to_disk() self.open_library(self.library_dir) @@ -465,16 +442,20 @@ def create_library(self, path) -> int: return 0 + def _fix_lib_path(self, path) -> Path: + """If '.TagStudio' is included in the path, trim the path up to it.""" + path = Path(path) + paths = [x for x in [path, *path.parents] if x.stem == TS_FOLDER_NAME] + if len(paths) > 0: + return paths[0].parent + return path + 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_collage_path = os.path.normpath( - f"{self.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}" - ) + full_ts_path = self.library_dir / TS_FOLDER_NAME + full_backup_path = self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME + full_collage_path = self.library_dir / TS_FOLDER_NAME / COLLAGE_FOLDER_NAME if not os.path.isdir(full_ts_path): os.mkdir(full_ts_path) @@ -501,28 +482,25 @@ 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: str | 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] + _path: Path = self._fix_lib_path(path) - if os.path.exists(os.path.normpath(f"{path}/{TS_FOLDER_NAME}/ts_library.json")): + if (_path / TS_FOLDER_NAME / "ts_library.json").exists(): try: with open( - os.path.normpath(f"{path}/{TS_FOLDER_NAME}/ts_library.json"), + _path / TS_FOLDER_NAME / "ts_library.json", "r", 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(".") @@ -602,6 +580,7 @@ def open_library(self, path: str) -> int: fields: list = [] if "fields" in entry: # Cast JSON str keys to ints + for f in entry["fields"]: f[int(list(f.keys())[0])] = f[list(f.keys())[0]] del f[list(f.keys())[0]] @@ -725,11 +704,7 @@ def open_library(self, path: str) -> int: # If the Library is loaded, continue other processes. if return_code == 1: - if not os.path.exists( - os.path.normpath(f"{self.library_dir}/{TS_FOLDER_NAME}") - ): - os.makedirs(os.path.normpath(f"{self.library_dir}/{TS_FOLDER_NAME}")) - + (self.library_dir / TS_FOLDER_NAME).mkdir(parents=True, exist_ok=True) self._map_filenames_to_entry_ids() return return_code @@ -739,19 +714,7 @@ 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[(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.""" @@ -813,9 +776,7 @@ def save_library_to_disk(self): self.verify_ts_folders() with open( - os.path.normpath(f"{self.library_dir}/{TS_FOLDER_NAME}/{filename}"), - "w", - encoding="utf-8", + self.library_dir / TS_FOLDER_NAME / filename, "w", encoding="utf-8" ) as outfile: outfile.flush() ujson.dump( @@ -841,9 +802,7 @@ def save_library_backup_to_disk(self) -> str: self.verify_ts_folders() with open( - os.path.normpath( - f"{self.library_dir}/{TS_FOLDER_NAME}/{BACKUP_FOLDER_NAME}/{filename}" - ), + self.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / filename, "w", encoding="utf-8", ) as outfile: @@ -878,7 +837,7 @@ def clear_internal_vars(self): self.files_not_in_library.clear() self.missing_files.clear() self.fixed_files.clear() - self.filename_to_entry_id_map = {} + self.filename_to_entry_id_map: dict[Path, int] = {} self.ignored_extensions = self.default_ext_blacklist self.tags.clear() @@ -901,23 +860,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) @@ -930,19 +885,16 @@ def refresh_dir(self) -> Generator: yield self.dir_file_count start_time = time.time() # print('') - # Sorts the files by date modified, descending. if len(self.files_not_in_library) <= 100000: 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: -(self.library_dir / t).stat().st_ctime, ) except (FileExistsError, FileNotFoundError): print( - f"[LIBRARY][ERROR] Couldn't sort files, some were moved during the scanning/sorting process." + f"[LIBRARY] [ERROR] Couldn't sort files, some were moved during the scanning/sorting process." ) pass else: @@ -954,11 +906,9 @@ def refresh_missing_files(self): """Tracks the number of Entries that point to an invalid file path.""" self.missing_files.clear() for i, entry in enumerate(self.entries): - full_path = os.path.normpath( - f"{self.library_dir}/{entry.path}/{entry.filename}" - ) - if not os.path.isfile(full_path): - self.missing_files.append(full_path) + full_path = self.library_dir / entry.path / entry.filename + if not full_path.is_file(): + self.missing_files.append(full_path.resolve()) yield i def remove_entry(self, entry_id: int) -> None: @@ -969,13 +919,9 @@ 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 = entry.path / entry.filename # logging.info(f'Removing path: {path}') + del self.filename_to_entry_id_map[path] del self.entries[self._entry_id_to_index_map[entry_id]] @@ -1016,22 +962,13 @@ def refresh_dupe_entries(self): 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 ( + 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( @@ -1078,37 +1015,36 @@ def merge_dupe_entries(self): ) else: sys.stdout.write( - f"\r[LIBRARY] [{i}/{len(self.entries)}] Consolidating Duplicate: {(e.path + os.pathsep + e.filename)[0:]}..." + 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 self._map_filenames_to_entry_ids() - def refresh_dupe_files(self, results_filepath): + def refresh_dupe_files(self, results_filepath: str | Path): """ Refreshes the list of duplicate files. A duplicate file is defined as an identical or near-identical file as determined by a DupeGuru results file. """ - full_results_path = ( - os.path.normpath(f"{self.library_dir}/{results_filepath}") - if self.library_dir not in results_filepath - else os.path.normpath(f"{results_filepath}") - ) - if os.path.exists(full_results_path): + 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 + + if full_results_path.is_file(): self.dupe_files.clear() self._map_filenames_to_entry_ids() tree = ET.parse(full_results_path) root = tree.getroot() for i, group in enumerate(root): # print(f'-------------------- Match Group {i}---------------------') - files: list[str] = [] + files: list[Path] = [] # (File Index, Matched File Index, Match Percentage) matches: list[tuple[int, int, int]] = [] for element in group: if element.tag == "file": - file = element.attrib.get("path") + file = Path(element.attrib.get("path")) files.append(file) if element.tag == "match": matches.append( @@ -1120,24 +1056,16 @@ 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]) - ) + file_1 = files[match[0]].relative_to(self.library_dir) + file_2 = files[match[1]].relative_to(self.library_dir) + + if ( + file_1.resolve 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("") @@ -1228,7 +1156,7 @@ def fix_missing_files(self): # json.dump({}, outfile, indent=4) # print(f'Re-saved to disk at {matched_json_filepath}') - def _match_missing_file(self, file: str) -> list[str]: + def _match_missing_file(self, file: str) -> list[Path]: """ Tries to find missing entry files within the library directory. Works if files were just moved to different subfolders and don't have duplicate names. @@ -1239,14 +1167,14 @@ def _match_missing_file(self, file: str) -> list[str]: matches = [] # for file in self.missing_files: - head, tail = os.path.split(file) - for root, dirs, files in os.walk(self.library_dir, topdown=True): + path = Path(file) + for root, dirs, files in os.walk(self.library_dir): for f in files: # print(f'{tail} --- {f}') - if tail == f and "$recycle.bin" not in root.lower(): + if path.name == f and "$recycle.bin" not in str(root).lower(): # self.fixed_files.append(tail) - new_path = str(os.path.relpath(root, self.library_dir)) + new_path = Path(root).relative_to(self.library_dir) matches.append(new_path) @@ -1255,7 +1183,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-> {self.library_dir / new_path / path.name}\n" ) if not matches: @@ -1263,16 +1191,39 @@ def _match_missing_file(self, file: str) -> list[str]: return matches + # print(f'╡ {os.path.normpath(os.path.relpath(file, self.library_dir))} ╞'.center( + # os.get_terminal_size()[0], "═")) + # print('↓ ↓ ↓'.center(os.get_terminal_size()[0], " ")) + # print( + # f'╡ {os.path.normpath(new_path + "/" + tail)} ╞'.center(os.get_terminal_size()[0], "═")) + # print(self.entries[self.file_to_entry_index_map[str( + # os.path.normpath(os.path.relpath(file, self.library_dir)))]]) + + # # print( + # # f'{file} -> {os.path.normpath(self.library_dir + "/" + new_path + "/" + tail)}') + # # # TODO: Update the Entry path with the 'new_path' variable via a completed update_entry() method. + + # if (str(os.path.normpath(new_path + "/" + tail))) in self.file_to_entry_index_map.keys(): + # print( + # 'Existing Entry ->'.center(os.get_terminal_size()[0], " ")) + # print(self.entries[self.file_to_entry_index_map[str( + # os.path.normpath(new_path + "/" + tail))]]) + + # print(f''.center(os.get_terminal_size()[0], "─")) + # print('') + + # for match in matches.keys(): + # self.fixed_files.append(match) + # # print(match) + # # print(f'\t{matches[match]}') + # with open( - # os.path.normpath( - # f"{self.library_dir}/{TS_FOLDER_NAME}/missing_matched.json" - # ), - # "w", + # self.library_dir / TS_FOLDER_NAME / "missing_matched.json", "w" # ) as outfile: - # outfile.flush() - # json.dump(matches, outfile, indent=4) + # outfile.flush() + # json.dump(matches, outfile, indent=4) # print( - # f'[LIBRARY] Saved to disk at {os.path.normpath(self.library_dir + "/" + TS_FOLDER_NAME + "/missing_matched.json")}' + # f'[LIBRARY] Saved to disk at {self.library_dir / TS_FOLDER_NAME / "missing_matched.json"}' # ) def count_tag_entry_refs(self) -> None: @@ -1314,10 +1265,10 @@ def add_new_files_as_entries(self) -> list[int]: """Adds files from the `files_not_in_library` list to the Library as Entries. Returns list of added indices.""" new_ids: list[int] = [] for file in self.files_not_in_library: - path, filename = os.path.split(file) + path = Path(file) # print(os.path.split(file)) entry = Entry( - id=self._next_entry_id, filename=filename, path=path, fields=[] + id=self._next_entry_id, filename=path.name, path=path.parent, fields=[] ) self._next_entry_id += 1 self.add_entry_to_library(entry) @@ -1348,18 +1299,10 @@ 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[ - str(os.path.normpath(os.path.relpath(filename, self.library_dir))) + Path(filename).relative_to(self.library_dir) ] - except: + except KeyError: return -1 def search_library( @@ -1416,10 +1359,7 @@ def search_library( # non_entry_count = 0 # Iterate over all Entries ============================================================= for entry in self.entries: - allowed_ext: bool = ( - os.path.splitext(entry.filename)[1][1:].lower() - not in self.ignored_extensions - ) + allowed_ext: bool = entry.filename.suffix not in self.ignored_extensions # try: # entry: Entry = self.entries[self.file_to_library_index_map[self._source_filenames[i]]] # print(f'{entry}') @@ -1453,11 +1393,8 @@ def search_library( results.append((ItemType.ENTRY, entry.id)) elif only_missing: if ( - os.path.normpath( - f"{self.library_dir}/{entry.path}/{entry.filename}" - ) - in self.missing_files - ): + self.library_dir / entry.path / entry.filename + ).resolve() in self.missing_files: results.append((ItemType.ENTRY, entry.id)) # elif query == "archived": @@ -1467,10 +1404,13 @@ def search_library( # elif query in entry.path.lower(): # NOTE: This searches path and filenames. + if allow_adv: - if [q for q in query_words if (q in entry.path.lower())]: + if [q for q in query_words if (q in str(entry.path).lower())]: results.append((ItemType.ENTRY, entry.id)) - elif [q for q in query_words if (q in entry.filename.lower())]: + elif [ + q for q in query_words if (q in str(entry.filename).lower()) + ]: results.append((ItemType.ENTRY, entry.id)) elif tag_only: if entry.has_tag(self, int(query_words[0])): @@ -1556,10 +1496,7 @@ def search_library( else: for entry in self.entries: added = False - allowed_ext = ( - os.path.splitext(entry.filename)[1][1:].lower() - not in self.ignored_extensions - ) + allowed_ext = entry.filename.suffix not in self.ignored_extensions if allowed_ext: for f in entry.fields: if self.get_field_attr(f, "type") == "collation": @@ -1624,7 +1561,7 @@ def search_tags( # NOTE: I'd expect a blank query to return all with the other implementation, but # it misses stuff like Archive (id 0) so here's this as a catch-all. - query = query.strip() + if not query: all: list[int] = [] for tag in self.tags: @@ -1931,13 +1868,13 @@ def get_tag_ref_count(self, tag_id: int) -> tuple[int, int]: # input() return (entry_ref_count, subtag_ref_count) - def update_entry_path(self, entry_id: int, path: str) -> None: + def update_entry_path(self, entry_id: int, path: str | Path) -> None: """Updates an Entry's path.""" - self.get_entry(entry_id).path = path + self.get_entry(entry_id).path = Path(path) - def update_entry_filename(self, entry_id: int, filename: str) -> None: + def update_entry_filename(self, entry_id: int, filename: str | Path) -> None: """Updates an Entry's filename.""" - self.get_entry(entry_id).filename = filename + self.get_entry(entry_id).filename = Path(filename) def update_entry_field(self, entry_id: int, field_index: int, content, mode: str): """Updates an Entry's specific field. Modes: append, remove, replace.""" diff --git a/tagstudio/src/core/ts_core.py b/tagstudio/src/core/ts_core.py index 8958b01f4..2644c9fdd 100644 --- a/tagstudio/src/core/ts_core.py +++ b/tagstudio/src/core/ts_core.py @@ -6,6 +6,7 @@ import json import os +from pathlib import Path from src.core.library import Entry, Library from src.core.constants import TS_FOLDER_NAME, TEXT_FIELDS @@ -20,7 +21,7 @@ class TagStudioCore: def __init__(self): self.lib: Library = Library() - def get_gdl_sidecar(self, filepath: str, source: str = "") -> dict: + def get_gdl_sidecar(self, filepath: str | Path, source: str = "") -> dict: """ Attempts to open and dump a Gallery-DL Sidecar sidecar file for the filepath.\n Returns a formatted object with notable values or an @@ -28,16 +29,19 @@ def get_gdl_sidecar(self, filepath: str, source: str = "") -> dict: """ json_dump = {} info = {} + _filepath: Path = Path(filepath) + _filepath = _filepath.parent / (_filepath.stem + ".json") # NOTE: This fixes an unknown (recent?) bug in Gallery-DL where Instagram sidecar # files may be downloaded with indices starting at 1 rather than 0, unlike the posts. # This may only occur with sidecar files that are downloaded separate from posts. if source == "instagram": - if not os.path.isfile(os.path.normpath(filepath + ".json")): - filepath = filepath[:-16] + "1" + filepath[-15:] + if not _filepath.is_file(): + newstem = _filepath.stem[:-16] + "1" + _filepath.stem[-15:] + _filepath = _filepath.parent / (newstem + ".json") try: - with open(os.path.normpath(filepath + ".json"), "r", encoding="utf8") as f: + with open(_filepath, "r", encoding="utf8") as f: json_dump = json.load(f) if json_dump: @@ -101,19 +105,17 @@ def get_gdl_sidecar(self, filepath: str, source: str = "") -> dict: def match_conditions(self, entry_id: int) -> None: """Matches defined conditions against a file to add Entry data.""" - cond_file = os.path.normpath( - f"{self.lib.library_dir}/{TS_FOLDER_NAME}/conditions.json" - ) + cond_file = self.lib.library_dir / TS_FOLDER_NAME / "conditions.json" # TODO: Make this stored somewhere better instead of temporarily in this JSON file. entry: Entry = self.lib.get_entry(entry_id) try: - if os.path.isfile(cond_file): + if cond_file.is_file(): with open(cond_file, "r", encoding="utf8") as f: json_dump = json.load(f) for c in json_dump["conditions"]: match: bool = False for path_c in c["path_conditions"]: - if os.path.normpath(path_c) in entry.path: + if str(Path(path_c).resolve()) in str(entry.path): match = True break if match: @@ -180,7 +182,7 @@ def _build_twitter_url(self, entry_id: int): """ try: entry = self.lib.get_entry(entry_id) - stubs = entry.filename.rsplit("_", 3) + stubs = str(entry.filename).rsplit("_", 3) # print(stubs) # source, author = os.path.split(entry.path) url = f"www.twitter.com/{stubs[0]}/status/{stubs[-3]}/photo/{stubs[-2]}" @@ -195,7 +197,7 @@ def _build_instagram_url(self, entry_id: int): """ try: entry = self.lib.get_entry(entry_id) - stubs = entry.filename.rsplit("_", 2) + stubs = str(entry.filename).rsplit("_", 2) # stubs[0] = stubs[0].replace(f"{author}_", '', 1) # print(stubs) # NOTE: Both Instagram usernames AND their ID can have underscores in them, diff --git a/tagstudio/src/qt/helpers/file_opener.py b/tagstudio/src/qt/helpers/file_opener.py index e60323aaa..76ef36666 100644 --- a/tagstudio/src/qt/helpers/file_opener.py +++ b/tagstudio/src/qt/helpers/file_opener.py @@ -8,6 +8,7 @@ import shutil import sys import traceback +from pathlib import Path from PySide6.QtWidgets import QLabel from PySide6.QtCore import Qt @@ -19,7 +20,7 @@ logging.basicConfig(format="%(message)s", level=logging.INFO) -def open_file(path: str, file_manager: bool = False): +def open_file(path: str | Path, file_manager: bool = False): """Open a file in the default application or file explorer. Args: @@ -27,13 +28,14 @@ def open_file(path: str, file_manager: bool = False): file_manager (bool, optional): Whether to open the file in the file manager (e.g. Finder on macOS). Defaults to False. """ - logging.info(f"Opening file: {path}") - if not os.path.exists(path): - logging.error(f"File not found: {path}") + _path = str(path) + logging.info(f"Opening file: {_path}") + if not os.path.exists(_path): + logging.error(f"File not found: {_path}") return try: if sys.platform == "win32": - normpath = os.path.normpath(path) + normpath = os.path.normpath(_path) if file_manager: command_name = "explorer" command_args = '/select,"' + normpath + '"' @@ -59,7 +61,7 @@ def open_file(path: str, file_manager: bool = False): else: if sys.platform == "darwin": command_name = "open" - command_args = [path] + command_args = [_path] if file_manager: # will reveal in Finder command_args.append("-R") @@ -73,12 +75,12 @@ def open_file(path: str, file_manager: bool = False): "--type=method_call", "/org/freedesktop/FileManager1", "org.freedesktop.FileManager1.ShowItems", - f"array:string:file://{path}", + f"array:string:file://{_path}", "string:", ] else: command_name = "xdg-open" - command_args = [path] + command_args = [_path] command = shutil.which(command_name) if command is not None: subprocess.Popen([command] + command_args, close_fds=True) @@ -89,21 +91,21 @@ def open_file(path: str, file_manager: bool = False): class FileOpenerHelper: - def __init__(self, filepath: str): + def __init__(self, filepath: str | Path): """Initialize the FileOpenerHelper. Args: filepath (str): The path to the file to open. """ - self.filepath = filepath + self.filepath = str(filepath) - def set_filepath(self, filepath: str): + def set_filepath(self, filepath: str | Path): """Set the filepath to open. Args: filepath (str): The path to the file to open. """ - self.filepath = filepath + self.filepath = str(filepath) def open_file(self): """Open the file in the default application.""" diff --git a/tagstudio/src/qt/main_window.py b/tagstudio/src/qt/main_window.py index 3f10ea00e..8987732da 100644 --- a/tagstudio/src/qt/main_window.py +++ b/tagstudio/src/qt/main_window.py @@ -23,6 +23,7 @@ class Ui_MainWindow(QMainWindow): + def __init__(self, parent=None) -> None: super().__init__(parent) self.setupUi(self) diff --git a/tagstudio/src/qt/modals/delete_unlinked.py b/tagstudio/src/qt/modals/delete_unlinked.py index 3787b18b4..83394805e 100644 --- a/tagstudio/src/qt/modals/delete_unlinked.py +++ b/tagstudio/src/qt/modals/delete_unlinked.py @@ -78,7 +78,7 @@ def refresh_list(self): self.model.clear() for i in self.lib.missing_files: - self.model.appendRow(QStandardItem(i)) + self.model.appendRow(QStandardItem(str(i))) def delete_entries(self): # pb = QProgressDialog('', None, 0, len(self.lib.missing_files)) diff --git a/tagstudio/src/qt/modals/fix_dupes.py b/tagstudio/src/qt/modals/fix_dupes.py index 2a85f97e2..b471f0763 100644 --- a/tagstudio/src/qt/modals/fix_dupes.py +++ b/tagstudio/src/qt/modals/fix_dupes.py @@ -137,9 +137,7 @@ def __init__(self, library: "Library", driver: "QtDriver"): self.set_dupe_count(self.count) def select_file(self): - qfd = QFileDialog( - self, "Open DupeGuru Results File", os.path.normpath(self.lib.library_dir) - ) + qfd = QFileDialog(self, "Open DupeGuru Results File", str(self.lib.library_dir)) qfd.setFileMode(QFileDialog.FileMode.ExistingFile) qfd.setNameFilter("DupeGuru Files (*.dupeguru)") if qfd.exec_(): diff --git a/tagstudio/src/qt/modals/folders_to_tags.py b/tagstudio/src/qt/modals/folders_to_tags.py index 1b9333119..a6e2c3f89 100644 --- a/tagstudio/src/qt/modals/folders_to_tags.py +++ b/tagstudio/src/qt/modals/folders_to_tags.py @@ -67,7 +67,7 @@ def add_folders_to_tree(items: list[str]) -> Tag: add_tag_to_tree(reversed_tag) for entry in library.entries: - folders = entry.path.split("\\") + folders = list(entry.path.parts) if len(folders) == 1 and folders[0] == "": continue tag = add_folders_to_tree(folders) @@ -120,7 +120,7 @@ def add_folders_to_tree(items: list[str]) -> dict: add_tag_to_tree(reversed_tag) for entry in library.entries: - folders = entry.path.split("\\") + folders = list(entry.path.parts) if len(folders) == 1 and folders[0] == "": continue branch = add_folders_to_tree(folders) diff --git a/tagstudio/src/qt/pagination.py b/tagstudio/src/qt/pagination.py index c877922bb..8d46b8ee0 100644 --- a/tagstudio/src/qt/pagination.py +++ b/tagstudio/src/qt/pagination.py @@ -145,7 +145,6 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): self.end_buffer_layout.itemAt(i).widget().setHidden(True) end_page = page_count - 1 - if page_count <= 1: # Hide everything if there are only one or less pages. # [-------------- HIDDEN --------------] @@ -178,6 +177,7 @@ def update_buttons(self, page_count: int, index: int, emit: bool = True): # self.start_buffer_layout.setContentsMargins(3,0,3,0) self._assign_click(self.prev_button, index - 1) self.prev_button.setDisabled(False) + if index == end_page: self.next_button.setDisabled(True) # self.end_buffer_layout.setContentsMargins(0,0,0,0) diff --git a/tagstudio/src/qt/ts_qt.py b/tagstudio/src/qt/ts_qt.py index a34e10eb5..ccdb73751 100644 --- a/tagstudio/src/qt/ts_qt.py +++ b/tagstudio/src/qt/ts_qt.py @@ -19,7 +19,6 @@ from pathlib import Path from queue import Queue from typing import Optional - from PIL import Image from PySide6 import QtCore from PySide6.QtCore import QObject, QThread, Signal, Qt, QThreadPool, QTimer, QSettings @@ -253,8 +252,8 @@ def start(self) -> None: # pal.setColor(QPalette.ColorGroup.Normal, # QPalette.ColorRole.Window, QColor('#110F1B')) # app.setPalette(pal) - home_path = os.path.normpath(f"{Path(__file__).parent}/ui/home.ui") - icon_path = os.path.normpath(f"{Path(__file__).parents[2]}/resources/icon.png") + home_path = Path(__file__).parent / "ui/home.ui" + icon_path = Path(__file__).parents[2] / "resources/icon.png" # Handle OS signals self.setup_signals() @@ -292,7 +291,7 @@ def start(self) -> None: if sys.platform != "darwin": icon = QIcon() - icon.addFile(icon_path) + icon.addFile(str(icon_path)) app.setWindowIcon(icon) menu_bar = QMenuBar(self.main_window) @@ -480,7 +479,6 @@ def start(self) -> None: lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio") ) help_menu.addAction(self.repo_action) - self.set_macro_menu_viability() menu_bar.addMenu(file_menu) @@ -495,9 +493,7 @@ def start(self) -> None: l.addWidget(self.preview_panel) QFontDatabase.addApplicationFont( - os.path.normpath( - f"{Path(__file__).parents[2]}/resources/qt/fonts/Oxanium-Bold.ttf" - ) + str(Path(__file__).parents[2] / "resources/qt/fonts/Oxanium-Bold.ttf") ) self.thumb_size = 128 @@ -677,7 +673,7 @@ def backup_library(self): fn = self.lib.save_library_backup_to_disk() end_time = time.time() self.main_window.statusbar.showMessage( - f'Library Backup Saved at: "{os.path.normpath(os.path.normpath(f"{self.lib.library_dir}/{TS_FOLDER_NAME}/{BACKUP_FOLDER_NAME}/{fn}"))}" ({format_timespan(end_time - start_time)})' + f'Library Backup Saved at: "{ self.lib.library_dir / TS_FOLDER_NAME / BACKUP_FOLDER_NAME / fn}" ({format_timespan(end_time - start_time)})' ) def add_tag_action_callback(self): @@ -847,8 +843,8 @@ def run_macros(self, name: str, entry_ids: list[int]): def run_macro(self, name: str, entry_id: int): """Runs a specific Macro on an Entry given a Macro name.""" entry = self.lib.get_entry(entry_id) - path = os.path.normpath(f"{self.lib.library_dir}/{entry.path}/{entry.filename}") - source = path.split(os.sep)[1].lower() + path = self.lib.library_dir / entry.path / entry.filename + source = entry.path.parts[0] if name == "sidecar": self.lib.add_generic_data_to_entry( self.core.get_gdl_sidecar(path, source), entry_id @@ -1200,9 +1196,7 @@ def update_thumbs(self): entry = self.lib.get_entry( self.nav_frames[self.cur_frame_idx].contents[i][1] ) - filepath = os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) + filepath = self.lib.library_dir / entry.path / entry.filename item_thumb.set_item_id(entry.id) item_thumb.assign_archived(entry.has_tag(self.lib, 0)) @@ -1247,9 +1241,7 @@ def update_thumbs(self): else collation.e_ids_and_pages[0][0] ) cover_e = self.lib.get_entry(cover_id) - filepath = os.path.normpath( - f"{self.lib.library_dir}/{cover_e.path}/{cover_e.filename}" - ) + filepath = self.lib.library_dir / cover_e.path / cover_e.filename item_thumb.set_count(str(len(collation.e_ids_and_pages))) item_thumb.update_clickable( clickable=( @@ -1548,8 +1540,11 @@ def try_save_collage(self, increment_progress: bool): self.completed += 1 # logging.info(f'threshold:{len(self.lib.entries}, completed:{self.completed}') if self.completed == len(self.lib.entries): - filename = os.path.normpath( - f'{self.lib.library_dir}/{TS_FOLDER_NAME}/{COLLAGE_FOLDER_NAME}/collage_{dt.utcnow().strftime("%F_%T").replace(":", "")}.png' + filename = ( + self.lib.library_dir + / TS_FOLDER_NAME + / COLLAGE_FOLDER_NAME + / f'collage_{dt.utcnow().strftime("%F_%T").replace(":", "")}.png' ) self.collage.save(filename) self.collage = None diff --git a/tagstudio/src/qt/ui/home_ui.py b/tagstudio/src/qt/ui/home_ui.py index 88d919fcf..93b83a0a4 100644 --- a/tagstudio/src/qt/ui/home_ui.py +++ b/tagstudio/src/qt/ui/home_ui.py @@ -8,54 +8,91 @@ ## WARNING! All changes made in this file will be lost when recompiling UI file! ################################################################################ -from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, - QMetaObject, QObject, QPoint, QRect, - QSize, QTime, QUrl, Qt) -from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, - QFont, QFontDatabase, QGradient, QIcon, - QImage, QKeySequence, QLinearGradient, QPainter, - QPalette, QPixmap, QRadialGradient, QTransform) -from PySide6.QtWidgets import (QApplication, QComboBox, QFrame, QGridLayout, - QHBoxLayout, QLayout, QLineEdit, QMainWindow, - QMenuBar, QPushButton, QScrollArea, QSizePolicy, - QStatusBar, QWidget) +from PySide6.QtCore import ( + QCoreApplication, + QDate, + QDateTime, + QLocale, + QMetaObject, + QObject, + QPoint, + QRect, + QSize, + QTime, + QUrl, + Qt, +) +from PySide6.QtGui import ( + QBrush, + QColor, + QConicalGradient, + QCursor, + QFont, + QFontDatabase, + QGradient, + QIcon, + QImage, + QKeySequence, + QLinearGradient, + QPainter, + QPalette, + QPixmap, + QRadialGradient, + QTransform, +) +from PySide6.QtWidgets import ( + QApplication, + QComboBox, + QFrame, + QGridLayout, + QHBoxLayout, + QLayout, + QLineEdit, + QMainWindow, + QMenuBar, + QPushButton, + QScrollArea, + QSizePolicy, + QStatusBar, + QWidget, +) + class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): - MainWindow.setObjectName(u"MainWindow") + MainWindow.setObjectName("MainWindow") MainWindow.resize(1280, 720) self.centralwidget = QWidget(MainWindow) - self.centralwidget.setObjectName(u"centralwidget") + self.centralwidget.setObjectName("centralwidget") self.gridLayout = QGridLayout(self.centralwidget) - self.gridLayout.setObjectName(u"gridLayout") + self.gridLayout.setObjectName("gridLayout") self.horizontalLayout = QHBoxLayout() - self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalLayout.setObjectName("horizontalLayout") self.scrollArea = QScrollArea(self.centralwidget) - self.scrollArea.setObjectName(u"scrollArea") + self.scrollArea.setObjectName("scrollArea") self.scrollArea.setFocusPolicy(Qt.WheelFocus) self.scrollArea.setFrameShape(QFrame.NoFrame) self.scrollArea.setFrameShadow(QFrame.Plain) self.scrollArea.setWidgetResizable(True) self.scrollAreaWidgetContents = QWidget() - self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") + self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 1260, 590)) self.gridLayout_2 = QGridLayout(self.scrollAreaWidgetContents) self.gridLayout_2.setSpacing(8) - self.gridLayout_2.setObjectName(u"gridLayout_2") + self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setContentsMargins(0, 0, 0, 8) self.scrollArea.setWidget(self.scrollAreaWidgetContents) self.horizontalLayout.addWidget(self.scrollArea) - self.gridLayout.addLayout(self.horizontalLayout, 5, 0, 1, 1) self.horizontalLayout_2 = QHBoxLayout() - self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") + self.horizontalLayout_2.setObjectName("horizontalLayout_2") self.horizontalLayout_2.setSizeConstraint(QLayout.SetMinimumSize) self.backButton = QPushButton(self.centralwidget) - self.backButton.setObjectName(u"backButton") + self.backButton.setObjectName("backButton") self.backButton.setMinimumSize(QSize(0, 32)) self.backButton.setMaximumSize(QSize(32, 16777215)) font = QFont() @@ -66,7 +103,7 @@ def setupUi(self, MainWindow): self.horizontalLayout_2.addWidget(self.backButton) self.forwardButton = QPushButton(self.centralwidget) - self.forwardButton.setObjectName(u"forwardButton") + self.forwardButton.setObjectName("forwardButton") self.forwardButton.setMinimumSize(QSize(0, 32)) self.forwardButton.setMaximumSize(QSize(32, 16777215)) font1 = QFont() @@ -78,7 +115,7 @@ def setupUi(self, MainWindow): self.horizontalLayout_2.addWidget(self.forwardButton) self.searchField = QLineEdit(self.centralwidget) - self.searchField.setObjectName(u"searchField") + self.searchField.setObjectName("searchField") self.searchField.setMinimumSize(QSize(0, 32)) font2 = QFont() font2.setPointSize(11) @@ -88,17 +125,16 @@ def setupUi(self, MainWindow): self.horizontalLayout_2.addWidget(self.searchField) self.searchButton = QPushButton(self.centralwidget) - self.searchButton.setObjectName(u"searchButton") + self.searchButton.setObjectName("searchButton") self.searchButton.setMinimumSize(QSize(0, 32)) self.searchButton.setFont(font2) self.horizontalLayout_2.addWidget(self.searchButton) - self.gridLayout.addLayout(self.horizontalLayout_2, 3, 0, 1, 1) self.comboBox = QComboBox(self.centralwidget) - self.comboBox.setObjectName(u"comboBox") + self.comboBox.setObjectName("comboBox") sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -111,11 +147,11 @@ def setupUi(self, MainWindow): MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) - self.menubar.setObjectName(u"menubar") + self.menubar.setObjectName("menubar") self.menubar.setGeometry(QRect(0, 0, 1280, 22)) MainWindow.setMenuBar(self.menubar) self.statusbar = QStatusBar(MainWindow) - self.statusbar.setObjectName(u"statusbar") + self.statusbar.setObjectName("statusbar") sizePolicy1 = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Maximum) sizePolicy1.setHorizontalStretch(0) sizePolicy1.setVerticalStretch(0) @@ -126,15 +162,24 @@ def setupUi(self, MainWindow): self.retranslateUi(MainWindow) QMetaObject.connectSlotsByName(MainWindow) + # setupUi def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) - self.backButton.setText(QCoreApplication.translate("MainWindow", u"<", None)) - self.forwardButton.setText(QCoreApplication.translate("MainWindow", u">", None)) - self.searchField.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Search Entries", None)) - self.searchButton.setText(QCoreApplication.translate("MainWindow", u"Search", None)) + MainWindow.setWindowTitle( + QCoreApplication.translate("MainWindow", "MainWindow", None) + ) + self.backButton.setText(QCoreApplication.translate("MainWindow", "<", None)) + self.forwardButton.setText(QCoreApplication.translate("MainWindow", ">", None)) + self.searchField.setPlaceholderText( + QCoreApplication.translate("MainWindow", "Search Entries", None) + ) + self.searchButton.setText( + QCoreApplication.translate("MainWindow", "Search", None) + ) self.comboBox.setCurrentText("") - self.comboBox.setPlaceholderText(QCoreApplication.translate("MainWindow", u"Thumbnail Size", None)) - # retranslateUi + self.comboBox.setPlaceholderText( + QCoreApplication.translate("MainWindow", "Thumbnail Size", None) + ) + # retranslateUi diff --git a/tagstudio/src/qt/widgets/collage_icon.py b/tagstudio/src/qt/widgets/collage_icon.py index 409e7fed0..b9234d7d2 100644 --- a/tagstudio/src/qt/widgets/collage_icon.py +++ b/tagstudio/src/qt/widgets/collage_icon.py @@ -52,9 +52,7 @@ def render( keep_aspect, ): entry = self.lib.get_entry(entry_id) - filepath = os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) + filepath = self.lib.library_dir / entry.path / entry.filename file_type = os.path.splitext(filepath)[1].lower()[1:] color: str = "" @@ -91,16 +89,14 @@ def render( self.rendered.emit(pic) if not data_only_mode: logging.info( - f"\r{INFO} Combining [ID:{entry_id}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}\033[0m" + f"\r{INFO} Combining [ID:{entry_id}/{len(self.lib.entries)}]: {self.get_file_color(filepath.suffix.lower())}{entry.path}{os.sep}{entry.filename}\033[0m" ) # sys.stdout.write(f'\r{INFO} Combining [{i+1}/{len(self.lib.entries)}]: {self.get_file_color(file_type)}{entry.path}{os.sep}{entry.filename}{RESET}') # sys.stdout.flush() - if file_type in IMAGE_TYPES: + if filepath.suffix.lower() in IMAGE_TYPES: try: with Image.open( - os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) + str(self.lib.library_dir / entry.path / entry.filename) ) as pic: if keep_aspect: pic.thumbnail(size) @@ -115,8 +111,8 @@ def render( self.rendered.emit(pic) except DecompressionBombError as e: logging.info(f"[ERROR] One of the images was too big ({e})") - elif file_type in VIDEO_TYPES: - video = cv2.VideoCapture(filepath) + elif filepath.suffix.lower() in VIDEO_TYPES: + video = cv2.VideoCapture(str(filepath)) video.set( cv2.CAP_PROP_POS_FRAMES, (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2), @@ -145,8 +141,9 @@ def render( f"\n{ERROR} Couldn't read {entry.path}{os.sep}{entry.filename}" ) with Image.open( - os.path.normpath( - f"{Path(__file__).parents[2]}/resources/qt/images/thumb_broken_512.png" + str( + Path(__file__).parents[2] + / "resources/qt/images/thumb_broken_512.png" ) ) as pic: pic.thumbnail(size) diff --git a/tagstudio/src/qt/widgets/fields.py b/tagstudio/src/qt/widgets/fields.py index c70390500..dfef94e20 100644 --- a/tagstudio/src/qt/widgets/fields.py +++ b/tagstudio/src/qt/widgets/fields.py @@ -18,23 +18,17 @@ class FieldContainer(QWidget): # TODO: reference a resources folder rather than path.parents[3]? clipboard_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/clipboard_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/clipboard_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) clipboard_icon_128.load() edit_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/edit_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/edit_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) edit_icon_128.load() trash_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/trash_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/trash_icon_128.png") ).resize((math.floor(24 * 1.25), math.floor(24 * 1.25))) trash_icon_128.load() diff --git a/tagstudio/src/qt/widgets/item_thumb.py b/tagstudio/src/qt/widgets/item_thumb.py index bcd0283d1..8cd238749 100644 --- a/tagstudio/src/qt/widgets/item_thumb.py +++ b/tagstudio/src/qt/widgets/item_thumb.py @@ -53,16 +53,12 @@ class ItemThumb(FlowWidget): update_cutoff: float = time.time() collation_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/collation_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/collation_icon_128.png") ) collation_icon_128.load() tag_group_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/tag_group_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/tag_group_icon_128.png") ) tag_group_icon_128.load() @@ -354,9 +350,11 @@ def set_mode(self, mode: Optional[ItemType]) -> None: # pass def set_extension(self, ext: str) -> None: - if ext and ext not in IMAGE_TYPES or ext in ["gif", "apng"]: + if ext and ext.startswith(".") is False: + ext = "." + ext + if ext and ext not in IMAGE_TYPES or ext in [".gif", ".apng"]: self.ext_badge.setHidden(False) - self.ext_badge.setText(ext.upper()) + self.ext_badge.setText(ext.upper()[1:]) if ext in VIDEO_TYPES + AUDIO_TYPES: self.count_badge.setHidden(False) else: @@ -414,9 +412,7 @@ def set_item_id(self, id: int): if id == -1: return entry = self.lib.get_entry(self.item_id) - filepath = os.path.normpath( - f"{self.lib.library_dir}/{entry.path}/{entry.filename}" - ) + filepath = self.lib.library_dir / entry.path / entry.filename self.opener.set_filepath(filepath) def assign_favorite(self, value: bool): diff --git a/tagstudio/src/qt/widgets/preview_panel.py b/tagstudio/src/qt/widgets/preview_panel.py index 049115af4..3d5290685 100644 --- a/tagstudio/src/qt/widgets/preview_panel.py +++ b/tagstudio/src/qt/widgets/preview_panel.py @@ -471,11 +471,9 @@ def update_widgets(self): item: Entry = self.lib.get_entry(self.driver.selected[0][1]) # If a new selection is made, update the thumbnail and filepath. if not self.selected or self.selected != self.driver.selected: - filepath = os.path.normpath( - f"{self.lib.library_dir}/{item.path}/{item.filename}" - ) + filepath = self.lib.library_dir / item.path / item.filename self.file_label.setFilePath(filepath) - window_title = filepath + window_title = str(filepath) ratio: float = self.devicePixelRatio() self.thumb_renderer.render( time.time(), @@ -484,7 +482,7 @@ def update_widgets(self): ratio, update_on_ratio_change=True, ) - self.file_label.setText("\u200b".join(filepath)) + self.file_label.setText("\u200b".join(str(filepath))) self.file_label.setCursor(Qt.CursorShape.PointingHandCursor) self.preview_img.setContextMenuPolicy( @@ -499,43 +497,53 @@ def update_widgets(self): ) # TODO: Do this somewhere else, this is just here temporarily. - extension = os.path.splitext(filepath)[1][1:].lower() try: image = None - if extension in IMAGE_TYPES: - image = Image.open(filepath) - elif extension in RAW_IMAGE_TYPES: + if filepath.suffix.lower() in IMAGE_TYPES: + image = Image.open(str(filepath)) + elif filepath.suffix.lower() in RAW_IMAGE_TYPES: with rawpy.imread(filepath) as raw: rgb = raw.postprocess() image = Image.new( "L", (rgb.shape[1], rgb.shape[0]), color="black" ) - elif extension in VIDEO_TYPES: - video = cv2.VideoCapture(filepath) + elif filepath.suffix.lower() in VIDEO_TYPES: + video = cv2.VideoCapture(str(filepath)) video.set(cv2.CAP_PROP_POS_FRAMES, 0) success, frame = video.read() frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) image = Image.fromarray(frame) # Stats for specific file types are displayed here. - if extension in (IMAGE_TYPES + VIDEO_TYPES + RAW_IMAGE_TYPES): + if filepath.suffix.lower() in ( + IMAGE_TYPES + VIDEO_TYPES + RAW_IMAGE_TYPES + ): self.dimensions_label.setText( - f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}\n{image.width} x {image.height} px" + f"{filepath.suffix.lower().upper()[1:]} • {format_size(os.stat(filepath).st_size)}\n{image.width} x {image.height} px" ) else: - self.dimensions_label.setText(f"{extension.upper()}") + self.dimensions_label.setText( + f"{filepath.suffix.lower().upper()[1:]} • {format_size(os.stat(filepath).st_size)}" + ) - if not image: - raise UnidentifiedImageError + if not filepath.is_file(): + raise FileNotFoundError + + except FileNotFoundError as e: + self.dimensions_label.setText( + f"{filepath.suffix.lower().upper()[1:]}" + ) + 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( - f"{extension.upper()} • {format_size(os.stat(filepath).st_size)}" + f"{filepath.suffix.lower().upper()[1:]} • {format_size(os.stat(filepath).st_size)}" ) logging.info( f"[PreviewPanel][ERROR] Couldn't Render thumbnail for {filepath} (because of {e})" diff --git a/tagstudio/src/qt/widgets/tag.py b/tagstudio/src/qt/widgets/tag.py index 09a0ecce5..739369dcf 100644 --- a/tagstudio/src/qt/widgets/tag.py +++ b/tagstudio/src/qt/widgets/tag.py @@ -24,9 +24,7 @@ class TagWidget(QWidget): edit_icon_128: Image.Image = Image.open( - os.path.normpath( - f"{Path(__file__).parents[3]}/resources/qt/images/edit_icon_128.png" - ) + str(Path(__file__).parents[3] / "resources/qt/images/edit_icon_128.png") ).resize((math.floor(14 * 1.25), math.floor(14 * 1.25))) edit_icon_128.load() on_remove = Signal() diff --git a/tagstudio/src/qt/widgets/thumb_renderer.py b/tagstudio/src/qt/widgets/thumb_renderer.py index fdf2c1be4..8dfa2e0d3 100644 --- a/tagstudio/src/qt/widgets/thumb_renderer.py +++ b/tagstudio/src/qt/widgets/thumb_renderer.py @@ -88,7 +88,7 @@ class ThumbRenderer(QObject): def render( self, timestamp: float, - filepath, + filepath: str | Path, base_size: tuple[int, int], pixel_ratio: float, is_loading=False, @@ -99,7 +99,7 @@ def render( image: Image.Image = None pixmap: QPixmap = None final: Image.Image = None - extension: str = None + _filepath: Path = Path(filepath) resampling_method = Image.Resampling.BILINEAR if ThumbRenderer.font_pixel_ratio != pixel_ratio: ThumbRenderer.font_pixel_ratio = pixel_ratio @@ -118,14 +118,12 @@ def render( pixmap.setDevicePixelRatio(pixel_ratio) if update_on_ratio_change: self.updated_ratio.emit(1) - elif filepath: - extension = os.path.splitext(filepath)[1][1:].lower() - + elif _filepath: try: # Images ======================================================= - if extension in IMAGE_TYPES: + if _filepath.suffix.lower() in IMAGE_TYPES: try: - image = Image.open(filepath) + image = Image.open(_filepath) if image.mode != "RGB" and image.mode != "RGBA": image = image.convert(mode="RGBA") if image.mode == "RGBA": @@ -136,12 +134,12 @@ def render( image = ImageOps.exif_transpose(image) except DecompressionBombError as e: logging.info( - f"[ThumbRenderer]{WARNING} Couldn't Render thumbnail for {filepath} (because of {e})" + f"[ThumbRenderer]{WARNING} Couldn't Render thumbnail for {_filepath} (because of {e})" ) - elif extension in RAW_IMAGE_TYPES: + elif _filepath.suffix.lower() in RAW_IMAGE_TYPES: try: - with rawpy.imread(filepath) as raw: + with rawpy.imread(str(_filepath)) as raw: rgb = raw.postprocess() image = Image.frombytes( "RGB", @@ -151,16 +149,16 @@ def render( ) except DecompressionBombError as e: logging.info( - f"[ThumbRenderer]{WARNING} Couldn't Render thumbnail for {filepath} (because of {e})" + f"[ThumbRenderer]{WARNING} Couldn't Render thumbnail for {_filepath} (because of {e})" ) except rawpy._rawpy.LibRawIOError: logging.info( - f"[ThumbRenderer]{ERROR} Couldn't Render thumbnail for raw image {filepath}" + f"[ThumbRenderer]{ERROR} Couldn't Render thumbnail for raw image {_filepath}" ) # Videos ======================================================= - elif extension in VIDEO_TYPES: - video = cv2.VideoCapture(filepath) + elif _filepath.suffix.lower() in VIDEO_TYPES: + video = cv2.VideoCapture(str(_filepath)) video.set( cv2.CAP_PROP_POS_FRAMES, (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2), @@ -176,8 +174,8 @@ def render( image = Image.fromarray(frame) # Plain Text =================================================== - elif extension in PLAINTEXT_TYPES: - with open(filepath, "r", encoding="utf-8") as text_file: + elif _filepath.suffix.lower() in PLAINTEXT_TYPES: + with open(_filepath, "r", encoding="utf-8") as text_file: text = text_file.read(256) bg = Image.new("RGB", (256, 256), color="#1e1e1e") draw = ImageDraw.Draw(bg) @@ -191,7 +189,7 @@ def render( # axes = figure.add_subplot(projection='3d') # # Load the STL files and add the vectors to the plot - # your_mesh = mesh.Mesh.from_file(filepath) + # your_mesh = mesh.Mesh.from_file(_filepath) # poly_collection = mplot3d.art3d.Poly3DCollection(your_mesh.vectors) # poly_collection.set_color((0,0,1)) # play with color @@ -230,7 +228,6 @@ def render( < max(base_size[0], base_size[1]) else Image.Resampling.BILINEAR ) - image = image.resize((new_x, new_y), resample=resampling_method) if gradient: mask: Image.Image = ThumbRenderer.thumb_mask_512.resize( @@ -268,19 +265,19 @@ def render( ) as e: if e is not UnicodeDecodeError: logging.info( - f"[ThumbRenderer]{ERROR}: Couldn't render thumbnail for {filepath} ({e})" + f"[ThumbRenderer]{ERROR}: Couldn't render thumbnail for {_filepath} ({e})" ) if update_on_ratio_change: self.updated_ratio.emit(1) final = ThumbRenderer.thumb_broken_512.resize( (adj_size, adj_size), resample=resampling_method ) - qim = ImageQt.ImageQt(final) if image: image.close() pixmap = QPixmap.fromImage(qim) pixmap.setDevicePixelRatio(pixel_ratio) + if pixmap: self.updated.emit( timestamp, @@ -289,8 +286,10 @@ def render( math.ceil(adj_size / pixel_ratio), math.ceil(final.size[1] / pixel_ratio), ), - extension, + _filepath.suffix.lower(), ) else: - self.updated.emit(timestamp, QPixmap(), QSize(*base_size), extension) + self.updated.emit( + timestamp, QPixmap(), QSize(*base_size), _filepath.suffix.lower() + )