refactor: modularize file_attributes.py

This commit is contained in:
Travis Abendshien
2024-12-31 03:27:27 -08:00
parent 3d7e0cb1bf
commit eba5583392
3 changed files with 126 additions and 244 deletions

View File

@@ -6,13 +6,12 @@ import os
import platform
import typing
from datetime import datetime as dt
from datetime import timedelta
from pathlib import Path
import cv2
import structlog
from humanfriendly import format_size
from PIL import ImageFont, UnidentifiedImageError
from PIL.Image import DecompressionBombError
from PIL import ImageFont
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QGuiApplication
from PySide6.QtWidgets import (
@@ -138,7 +137,7 @@ class FileAttributes(QWidget):
self.date_created_label.setHidden(True)
self.date_modified_label.setHidden(True)
def update_stats(self, filepath: Path | None = None, stats: dict = None):
def update_stats(self, filepath: Path | None = None, ext: str = ".", stats: dict = None):
"""Render the panel widgets with the newest data from the Library."""
logger.info("update_stats", selected=filepath)
@@ -169,52 +168,68 @@ class FileAttributes(QWidget):
# Translations.translate_qobject(self.open_file_action, "file.open_file")
# self.open_explorer_action = QAction(PlatformStrings.open_file_str, self)
# TODO: Do this all somewhere else, this is just here temporarily.
ext: str = filepath.suffix.lower()
try:
self.dimensions_label.setText(
f"{ext.upper()[1:]}{format_size(filepath.stat().st_size)}\n"
f"{stats.get("width")} x {stats.get("height")} px"
)
if MediaCategories.is_ext_in_category(
ext, MediaCategories.FONT_TYPES, mime_fallback=True
):
try:
# Initialize the possible stat variables
stats_label_text = ""
ext_display: str = ""
file_size: str = ""
width_px_text: str = ""
height_px_text: str = ""
duration_text: str = ""
font_family: str = ""
# Attempt to populate the stat variables
width_px_text = stats.get("width", "")
height_px_text = stats.get("height", "")
duration_text = stats.get("duration", "")
font_family = stats.get("font_family", "")
if ext:
ext_display = ext.upper()[1:]
if filepath:
try:
file_size = format_size(filepath.stat().st_size)
if MediaCategories.is_ext_in_category(
ext, MediaCategories.FONT_TYPES, mime_fallback=True
):
font = ImageFont.truetype(filepath)
self.dimensions_label.setText(
f"{ext.upper()[1:]}{format_size(filepath.stat().st_size)}\n"
f"{font.getname()[0]} ({font.getname()[1]}) "
)
except OSError:
self.dimensions_label.setText(
f"{ext.upper()[1:]}{format_size(filepath.stat().st_size)}"
)
logger.info(f"[PreviewPanel][ERROR] Couldn't read font file: {filepath}")
else:
self.dimensions_label.setText(f"{ext.upper()[1:]}")
self.dimensions_label.setText(
f"{ext.upper()[1:]}{format_size(filepath.stat().st_size)}"
font_family = f"{font.getname()[0]} ({font.getname()[1]}) "
except (FileNotFoundError, OSError) as e:
logger.error(
"[FileAttributes] Could not process file stats", filepath=filepath, error=e
)
# self.update_date_label(filepath)
if not filepath.is_file():
raise FileNotFoundError
# Format and display any stat variables
def add_newline(stats_label_text: str) -> str:
logger.info(stats_label_text[-2:])
if stats_label_text and stats_label_text[-2:] != "\n":
return stats_label_text + "\n"
return stats_label_text
except (FileNotFoundError, cv2.error) as e:
self.dimensions_label.setText(f"{ext.upper()[1:]}")
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
# self.update_date_label()
except (
UnidentifiedImageError,
DecompressionBombError, # noqa: F821
) as e:
self.dimensions_label.setText(
f"{ext.upper()[1:]}{format_size(filepath.stat().st_size)}"
)
logger.error("Couldn't render thumbnail", filepath=filepath, error=e)
# self.update_date_label(filepath)
if ext_display:
stats_label_text += ext_display
if file_size:
stats_label_text += f"{file_size}"
elif file_size:
stats_label_text += file_size
return stats
if width_px_text and height_px_text:
stats_label_text = add_newline(stats_label_text)
stats_label_text += f"{width_px_text} x {height_px_text} px"
if duration_text:
stats_label_text = add_newline(stats_label_text)
dur_str = str(timedelta(seconds=float(duration_text)))[:-7]
if dur_str.startswith("0:"):
dur_str = dur_str[2:]
if dur_str.startswith("0"):
dur_str = dur_str[1:]
stats_label_text += f"{dur_str}"
if font_family:
stats_label_text = add_newline(stats_label_text)
stats_label_text += f"{font_family}"
self.dimensions_label.setText(stats_label_text)
def update_multi_selection(self, count: int):
# Multiple Selected Items

View File

@@ -51,13 +51,6 @@ class PreviewThumb(QWidget):
self.img_button_size: tuple[int, int] = (266, 266)
self.image_ratio: float = 1.0
# self.panel_bg_color = (
# Theme.COLOR_BG_DARK.value
# if QGuiApplication.styleHints().colorScheme() is Qt.ColorScheme.Dark
# else Theme.COLOR_BG_LIGHT.value
# )
# self.image_container = QWidget()
image_layout = QHBoxLayout(self)
image_layout.setContentsMargins(0, 0, 0, 0)
@@ -171,6 +164,22 @@ class PreviewThumb(QWidget):
self.gif_buffer.close()
self.preview_gif.hide()
def _display_fallback_image(self, filepath: Path, ext=str) -> dict:
"""Renders the given file as an image, no matter its media type.
Useful for fallback scenarios.
"""
self.switch_preview("image")
self.thumb_renderer.render(
time.time(),
filepath,
(512, 512),
self.devicePixelRatio(),
update_on_ratio_change=True,
)
self.preview_img.show()
return self._update_image(filepath, ext)
def _update_image(self, filepath: Path, ext: str) -> dict:
"""Update the static image preview from a filepath."""
stats: dict = {}
@@ -199,8 +208,8 @@ class PreviewThumb(QWidget):
image = Image.open(str(filepath))
stats["width"] = image.width
stats["height"] = image.height
except UnidentifiedImageError:
logger.error("welp", filepath=filepath)
except UnidentifiedImageError as e:
logger.error("[PreviewThumb] Could not get image stats", filepath=filepath, error=e)
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_VECTOR_TYPES, mime_fallback=True
):
@@ -219,51 +228,48 @@ class PreviewThumb(QWidget):
self.preview_gif.movie().stop()
self.gif_buffer.close()
image: Image.Image = Image.open(filepath)
stats["width"] = image.width
stats["height"] = image.height
self.update_image_size((image.width, image.height), image.width / image.height)
anim_image: Image.Image = image
image_bytes_io: io.BytesIO = io.BytesIO()
anim_image.save(
image_bytes_io,
"GIF",
lossless=True,
save_all=True,
loop=0,
disposal=2,
)
image_bytes_io.seek(0)
ba: bytes = image_bytes_io.read()
self.gif_buffer.setData(ba)
movie = QMovie(self.gif_buffer, QByteArray())
self.preview_gif.setMovie(movie)
# If the animation only has 1 frame, display it like a normal image.
if movie.frameCount() == 1:
self.switch_preview("image")
self.thumb_renderer.render(
time.time(),
filepath,
(512, 512),
self.devicePixelRatio(),
update_on_ratio_change=True,
try:
image: Image.Image = Image.open(filepath)
stats["width"] = image.width
stats["height"] = image.height
self.update_image_size((image.width, image.height), image.width / image.height)
anim_image: Image.Image = image
image_bytes_io: io.BytesIO = io.BytesIO()
anim_image.save(
image_bytes_io,
"GIF",
lossless=True,
save_all=True,
loop=0,
disposal=2,
)
self.preview_img.show()
return stats
image_bytes_io.seek(0)
ba: bytes = image_bytes_io.read()
self.gif_buffer.setData(ba)
movie = QMovie(self.gif_buffer, QByteArray())
self.preview_gif.setMovie(movie)
# The animation has more than 1 frame, continue displaying it as an animation
self.switch_preview("animated")
self.resizeEvent(
QResizeEvent(
QSize(image.width, image.height),
QSize(image.width, image.height),
# If the animation only has 1 frame, display it like a normal image.
if movie.frameCount() == 1:
self._display_fallback_image(filepath, ext)
return stats
# The animation has more than 1 frame, continue displaying it as an animation
self.switch_preview("animated")
self.resizeEvent(
QResizeEvent(
QSize(image.width, image.height),
QSize(image.width, image.height),
)
)
)
movie.start()
self.preview_gif.show()
movie.start()
self.preview_gif.show()
stats["duration"] = movie.frameCount() // 60
except UnidentifiedImageError as e:
logger.error("[PreviewThumb] Could not load animated image", filepath=filepath, error=e)
return self._display_fallback_image(filepath, ext)
stats["duration"] = movie.frameCount() // 60
return stats
def _update_video_legacy(self, filepath: Path) -> dict:
@@ -302,20 +308,20 @@ class PreviewThumb(QWidget):
self.media_player.show()
self.media_player.play(filepath)
stats["duration"] = self.media_player.player.duration()
stats["duration"] = self.media_player.player.duration() * 1000
return stats
def update_preview(self, filepath: Path) -> dict:
def update_preview(self, filepath: Path, ext: str) -> dict:
"""Render a single file preview."""
stats: dict = {}
ext: str = filepath.suffix.lower()
stats["ext"] = ext
# Video (Legacy)
if MediaCategories.is_ext_in_category(
ext, MediaCategories.VIDEO_TYPES, mime_fallback=True
) and is_readable_video(filepath):
stats = self._update_video_legacy(filepath)
# Audio
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.AUDIO_TYPES, mime_fallback=True
):
@@ -329,11 +335,13 @@ class PreviewThumb(QWidget):
update_on_ratio_change=True,
)
# Animated Images
elif MediaCategories.is_ext_in_category(
ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
):
stats = self._update_animation(filepath, ext)
# Other Types (Including Images)
else:
# TODO: Get thumb renderer to return this stuff to pass on
stats = self._update_image(filepath, ext)
@@ -360,142 +368,6 @@ class PreviewThumb(QWidget):
return stats
# self.tag_callback = tag_callback if tag_callback else None
# # update list of libraries
# self.fill_libs_widget(self.libs_layout)
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.NoContextMenu)
# self.preview_img.setCursor(Qt.CursorShape.ArrowCursor)
# ratio = self.devicePixelRatio()
# self.thumb_renderer.render(
# time.time(),
# "",
# (512, 512),
# ratio,
# is_loading=True,
# update_on_ratio_change=True,
# )
# if self.preview_img.is_connected:
# self.preview_img.clicked.disconnect()
# self.preview_img.show()
# self.preview_vid.stop()
# self.preview_vid.hide()
# self.media_player.hide()
# self.media_player.stop()
# self.preview_gif.hide()
# self.selected = list(self.driver.selected)
# self.add_field_button.setHidden(True)
# reload entry and fill it into the grid again
# 1 Selected Entry
# selected_idx = self.driver.selected[0]
# item = self.driver.frame_content[selected_idx]
# If a new selection is made, update the thumbnail and filepath.
# ratio = self.devicePixelRatio()
# self.thumb_renderer.render(
# time.time(),
# filepath,
# (512, 512),
# ratio,
# update_on_ratio_change=True,
# )
# self.preview_img.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
# self.preview_img.setCursor(Qt.CursorShape.PointingHandCursor)
# self.opener = FileOpenerHelper(filepath)
# self.open_file_action.triggered.connect(self.opener.open_file)
# self.open_explorer_action.triggered.connect(self.opener.open_explorer)
# # TODO: Do this all somewhere else, this is just here temporarily.
# ext: str = filepath.suffix.lower()
# try:
# if MediaCategories.is_ext_in_category(
# ext, MediaCategories.IMAGE_ANIMATED_TYPES, mime_fallback=True
# ):
# if self.preview_gif.movie():
# self.preview_gif.movie().stop()
# self.gif_buffer.close()
# image: Image.Image = Image.open(filepath)
# anim_image: Image.Image = image
# image_bytes_io: io.BytesIO = io.BytesIO()
# anim_image.save(
# image_bytes_io,
# "GIF",
# lossless=True,
# save_all=True,
# loop=0,
# disposal=2,
# )
# image_bytes_io.seek(0)
# ba: bytes = image_bytes_io.read()
# self.gif_buffer.setData(ba)
# movie = QMovie(self.gif_buffer, QByteArray())
# self.preview_gif.setMovie(movie)
# movie.start()
# # self.resizeEvent(
# # QResizeEvent(
# # QSize(image.width, image.height),
# # QSize(image.width, image.height),
# # )
# # )
# self.preview_img.hide()
# self.preview_vid.hide()
# self.preview_gif.show()
# image = None
# if MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RASTER_TYPES):
# image = Image.open(str(filepath))
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.IMAGE_RAW_TYPES):
# try:
# with rawpy.imread(str(filepath)) as raw:
# rgb = raw.postprocess()
# image = Image.new("L", (rgb.shape[1], rgb.shape[0]), color="black")
# except (
# rawpy._rawpy.LibRawIOError,
# rawpy._rawpy.LibRawFileUnsupportedError,
# ):
# pass
# elif MediaCategories.is_ext_in_category(ext, MediaCategories.AUDIO_TYPES):
# self.media_player.show()
# self.media_player.play(filepath)
# elif MediaCategories.is_ext_in_category(
# ext, MediaCategories.VIDEO_TYPES
# ) and is_readable_video(filepath):
# video = cv2.VideoCapture(str(filepath), cv2.CAP_FFMPEG)
# video.set(
# cv2.CAP_PROP_POS_FRAMES,
# (video.get(cv2.CAP_PROP_FRAME_COUNT) // 2),
# )
# success, frame = video.read()
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# image = Image.fromarray(frame)
# if success:
# self.preview_img.hide()
# self.preview_vid.play(str(filepath), QSize(image.width, image.height))
# # self.resizeEvent(
# # QResizeEvent(
# # QSize(image.width, image.height),
# # QSize(image.width, image.height),
# # )
# # )
# self.preview_vid.show()
# except (FileNotFoundError, cv2.error, UnidentifiedImageError, DecompressionBombError) as e:
# if self.preview_img.is_connected:
# self.preview_img.clicked.disconnect()
# self.preview_img.clicked.connect(lambda checked=False, pth=filepath: open_file(pth))
# self.preview_img.is_connected = True
# logger.error(f"Preview thumb error: {e} - {filepath}")
# return stats
def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
self.update_image_size((self.size().width(), self.size().height()))
return super().resizeEvent(event)

View File

@@ -82,12 +82,6 @@ class PreviewPanel(QWidget):
)
self.driver.frame_content[grid_idx] = result
# def resizeEvent(self, event: QResizeEvent) -> None: # noqa: N802
# # self.thumb.update_image_size(
# # (self.thumb.image_container.size().width(), self.thumb.image_container.size().height())
# # )
# return super().resizeEvent(event)
def update_widgets(self) -> bool:
"""Render the panel widgets with the newest data from the Library."""
# No Items Selected
@@ -101,10 +95,11 @@ class PreviewPanel(QWidget):
elif len(self.driver.selected) == 1:
entry: Entry = items[0]
filepath: Path = self.lib.library_dir / entry.path
ext: str = filepath.suffix.lower()
stats: dict = self.thumb.update_preview(filepath)
logger.info(stats)
self.file_attrs.update_stats(filepath)
stats: dict = self.thumb.update_preview(filepath, ext)
logger.info("stats", stats=stats, ext=ext)
self.file_attrs.update_stats(filepath, ext, stats)
self.file_attrs.update_date_label(filepath)
# TODO: Render regular single selection
# TODO: Return known attributes from thumb, and give those to field_attrs