Replace use of os.path with pathlib (#156)

* Replace usage of os.path with usage of pathlib.Path in ts_cli.py

* Replace use of os.path with pathlib in Library.py

* Replace use of os.path with pathlib in ts_core.py

* resolve entry path/filename on creation/update

* Fix errors and bugs related to move from os.path to pathlib.

* Remove most uses of '.resolve()' as it didn't do what I thought it did

* Fix filtering in refresh directories to not need to cast to string.

* Some work on ts_qt, thumbnails don't load...

* Fixed the thumbnail issue, things seem to be working.

* Fix some bugs

* Replace some isfile with is_file ts_cli.py

* Update tagstudio/src/core/library.py

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>

* Update library.py

* Update library.py

* Update library.py

* Update ts_cli.py

* Update library.py

* Update ts_qt

* Fix path display in delete unlinked entries modal

* Ruff formatting

* Builds and opens/creates library now

* Fix errors

* Fix ruff and mypy issues (hopefully)

* Fixed some things, broke some things

* Fixed the thumbnails not working

* Fix some new os.path instances in qt files

* Fix MyPy issues

* Fix ruff and mypy issues

* Fix some issues

* Update tagstudio/src/qt/widgets/preview_panel.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tagstudio/src/qt/widgets/preview_panel.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tagstudio/src/qt/widgets/preview_panel.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tagstudio/src/qt/widgets/preview_panel.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tagstudio/src/qt/widgets/thumb_renderer.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Update tagstudio/src/qt/widgets/thumb_renderer.py

Co-authored-by: Travis Abendshien <46939827+CyanVoxel@users.noreply.github.com>

* Fix refresh_dupe_files issue

* Ruff format

* Tweak filepaths

- Suffix comparisons are now case-insensitive
- Restore original thumbnail extension label behavior
- Fix preview panel trying to read file size from missing files

---------

Co-authored-by: yed podtrzitko <yedpodtrzitko@users.noreply.github.com>
Co-authored-by: Travis Abendshien <lvnvtravis@gmail.com>
This commit is contained in:
Icosahunter
2024-05-26 18:17:05 -05:00
committed by GitHub
parent 2e8678414b
commit 6798ffd0a7
18 changed files with 471 additions and 506 deletions

View File

@@ -88,21 +88,19 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
"""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 @@ class CliDriver:
"""
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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
"""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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
)
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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
) > 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 @@ class CliDriver:
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 @@ class CliDriver:
# 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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
# 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 @@ class CliDriver:
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 @@ class CliDriver:
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 @@ class CliDriver:
)
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 @@ class CliDriver:
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 @@ class CliDriver:
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}"
)

View File

@@ -9,76 +9,86 @@ LIBRARY_FILENAME: str = "ts_library.json"
# 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

View File

@@ -20,6 +20,7 @@ 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
@@ -48,11 +49,13 @@ logging.basicConfig(format="%(message)s", level=logging.INFO)
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 @@ class Entry:
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 @@ class Entry:
"""
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 @@ class Collation:
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 @@ class Library:
# 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 @@ class Library:
2: File creation error
"""
path = os.path.normpath(path).rstrip("\\")
# If '.TagStudio' is included in the path, trim the path up to it.
if TS_FOLDER_NAME in 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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
# 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 @@ class Library:
"""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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
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 @@ class Library:
# - 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 @@ class Library:
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 @@ class Library:
"""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 @@ class Library:
# 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 @@ class Library:
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 @@ class Library:
)
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 @@ class Library:
)
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 @@ class Library:
# 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 @@ class Library:
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 @@ class Library:
# 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 @@ class Library:
return matches
# with open(
# os.path.normpath(
# f"{self.library_dir}/{TS_FOLDER_NAME}/missing_matched.json"
# ),
# "w",
# ) as outfile:
# outfile.flush()
# json.dump(matches, outfile, indent=4)
# 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'[LIBRARY] Saved to disk at {os.path.normpath(self.library_dir + "/" + TS_FOLDER_NAME + "/missing_matched.json")}'
# 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(
# self.library_dir / TS_FOLDER_NAME / "missing_matched.json", "w"
# ) as outfile:
# outfile.flush()
# json.dump(matches, outfile, indent=4)
# print(
# 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 @@ class Library:
"""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 @@ class Library:
"""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 @@ class 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 @@ class 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 @@ class 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 @@ class 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 @@ class Library:
# 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 @@ class Library:
# 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."""

View File

@@ -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 @@ class TagStudioCore:
"""
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 @@ class TagStudioCore:
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 @@ class TagStudioCore:
"""
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 @@ class TagStudioCore:
"""
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,

View File

@@ -8,6 +8,7 @@ import subprocess
import shutil
import sys
import traceback
from pathlib import Path
from PySide6.QtWidgets import QLabel
from PySide6.QtCore import Qt
@@ -19,7 +20,7 @@ INFO = f"[INFO]"
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."""

View File

@@ -23,6 +23,7 @@ from src.qt.pagination import Pagination
class Ui_MainWindow(QMainWindow):
def __init__(self, parent=None) -> None:
super().__init__(parent)
self.setupUi(self)

View File

@@ -78,7 +78,7 @@ class DeleteUnlinkedEntriesModal(QWidget):
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))

View File

@@ -137,9 +137,7 @@ class FixDupeFilesModal(QWidget):
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_():

View File

@@ -67,7 +67,7 @@ def folders_to_tags(library: Library):
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 generate_preview_data(library: Library):
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)

View File

@@ -145,7 +145,6 @@ class Pagination(QWidget, QObject):
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 @@ class Pagination(QWidget, QObject):
# 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)

View File

@@ -19,7 +19,6 @@ from datetime import datetime as dt
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 @@ class QtDriver(QObject):
# 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 @@ class QtDriver(QObject):
if sys.platform != "darwin":
icon = QIcon()
icon.addFile(icon_path)
icon.addFile(str(icon_path))
app.setWindowIcon(icon)
menu_bar = QMenuBar(self.main_window)
@@ -493,7 +492,6 @@ class QtDriver(QObject):
lambda: webbrowser.open("https://github.com/TagStudioDev/TagStudio")
)
help_menu.addAction(self.repo_action)
self.set_macro_menu_viability()
menu_bar.addMenu(file_menu)
@@ -508,9 +506,7 @@ class QtDriver(QObject):
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
@@ -690,7 +686,7 @@ class QtDriver(QObject):
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):
@@ -869,8 +865,8 @@ class QtDriver(QObject):
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
@@ -1222,9 +1218,7 @@ class QtDriver(QObject):
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))
@@ -1269,9 +1263,7 @@ class QtDriver(QObject):
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=(
@@ -1570,8 +1562,11 @@ class QtDriver(QObject):
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

View File

@@ -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 @@ class Ui_MainWindow(object):
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 @@ class Ui_MainWindow(object):
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 @@ class Ui_MainWindow(object):
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 @@ class Ui_MainWindow(object):
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 @@ class Ui_MainWindow(object):
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

View File

@@ -52,9 +52,7 @@ class CollageIconRenderer(QObject):
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 @@ class CollageIconRenderer(QObject):
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 @@ class CollageIconRenderer(QObject):
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 @@ class CollageIconRenderer(QObject):
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)

View File

@@ -18,23 +18,17 @@ from PySide6.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushBu
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()

View File

@@ -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 @@ class ItemThumb(FlowWidget):
# 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 @@ class ItemThumb(FlowWidget):
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):

View File

@@ -471,11 +471,9 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
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 @@ class PreviewPanel(QWidget):
)
# 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})"

View File

@@ -24,9 +24,7 @@ INFO = f"[INFO]"
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()

View File

@@ -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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
)
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 @@ class ThumbRenderer(QObject):
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 @@ class ThumbRenderer(QObject):
# 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 @@ class ThumbRenderer(QObject):
< 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 @@ class ThumbRenderer(QObject):
) 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 @@ class ThumbRenderer(QObject):
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()
)