""" Metadata Manager Module for Bagheera. This module provides a dedicated class for handling various metadata formats like EXIF, IPTC, and XMP, using the exiv2 library. Classes: MetadataManager: A class with static methods to read metadata from files. """ import os from PySide6.QtDBus import QDBusConnection, QDBusMessage, QDBus try: import exiv2 HAVE_EXIV2 = True except ImportError: exiv2 = None HAVE_EXIV2 = False from utils import preserve_mtime def notify_baloo(path): """ Notifies the Baloo file indexer about a file change using DBus. This is an asynchronous, non-blocking call. It's more efficient than calling `balooctl` via subprocess. Args: path (str): The absolute path of the file that was modified. """ if not path: return # Use QDBusMessage directly for robust calling msg = QDBusMessage.createMethodCall( "org.kde.baloo.file", "/org/kde/baloo/file", "org.kde.baloo.file.indexer", "indexFile" ) msg.setArguments([path]) QDBusConnection.sessionBus().call(msg, QDBus.NoBlock) class MetadataManager: """Manages reading EXIF, IPTC, and XMP metadata.""" @staticmethod def read_all_metadata(path): """ Reads all available EXIF, IPTC, and XMP metadata from a file. Args: path (str): The path to the image file. Returns: dict: A dictionary containing all found metadata key-value pairs. Returns an empty dictionary if exiv2 is not available or on error. """ if not HAVE_EXIV2: return {} all_metadata = {} try: image = exiv2.ImageFactory.open(path) image.readMetadata() # EXIF for datum in image.exifData(): if datum.toString(): all_metadata[datum.key()] = datum.toString() # IPTC for datum in image.iptcData(): if datum.toString(): all_metadata[datum.key()] = datum.toString() # XMP for datum in image.xmpData(): if datum.toString(): all_metadata[datum.key()] = datum.toString() except Exception as e: print(f"Error reading metadata for {path}: {e}") return all_metadata class XattrManager: """A manager class to handle reading and writing extended attributes (xattrs).""" @staticmethod def get_attribute(path_or_fd, attr_name, default_value=""): """ Gets a string value from a file's extended attribute. This is a disk read. Args: path_or_fd (str or int): The path to the file or a file descriptor. attr_name (str): The name of the extended attribute. default_value (any): The value to return if the attribute is not found. Returns: str: The attribute value or the default value. """ if path_or_fd is None or path_or_fd == "": return default_value try: return os.getxattr(path_or_fd, attr_name).decode('utf-8') except (OSError, AttributeError): return default_value @staticmethod def set_attribute(file_path, attr_name, value): """ Sets a string value for a file's extended attribute. If the value is None or an empty string, the attribute is removed. Args: file_path (str): The path to the file. attr_name (str): The name of the extended attribute. value (str or None): The value to set. Raises: IOError: If the attribute could not be saved. """ if not file_path: return try: with preserve_mtime(file_path): if value: os.setxattr(file_path, attr_name, str(value).encode('utf-8')) else: try: os.removexattr(file_path, attr_name) except OSError: pass notify_baloo(file_path) except Exception as e: raise IOError(f"Could not save xattr '{attr_name}' " "for {file_path}: {e}") from e