mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-01-30 23:00:51 +00:00
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:
@@ -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}"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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_():
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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})"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user