188 lines
5.6 KiB
Python
188 lines
5.6 KiB
Python
"""
|
|
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
|
|
from constants import RATING_XATTR_NAME, XATTR_NAME
|
|
|
|
|
|
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)
|
|
|
|
|
|
def load_common_metadata(path):
|
|
"""
|
|
Loads tag and rating data for a path using extended attributes.
|
|
"""
|
|
tags = []
|
|
raw_tags = XattrManager.get_attribute(path, XATTR_NAME)
|
|
if raw_tags:
|
|
tags = sorted(list(set(t.strip()
|
|
for t in raw_tags.split(',') if t.strip())))
|
|
|
|
raw_rating = XattrManager.get_attribute(path, RATING_XATTR_NAME, "0")
|
|
try:
|
|
rating = int(raw_rating)
|
|
except ValueError:
|
|
rating = 0
|
|
return tags, rating
|
|
|
|
|
|
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
|
|
|
|
@staticmethod
|
|
def get_all_attributes(path):
|
|
"""
|
|
Gets all extended attributes for a file as a dictionary.
|
|
|
|
Args:
|
|
path (str): The path to the file.
|
|
|
|
Returns:
|
|
dict: A dictionary mapping attribute names to values.
|
|
"""
|
|
attributes = {}
|
|
if not path:
|
|
return attributes
|
|
try:
|
|
keys = os.listxattr(path)
|
|
for key in keys:
|
|
try:
|
|
val = os.getxattr(path, key)
|
|
try:
|
|
val_str = val.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
val_str = str(val)
|
|
attributes[key] = val_str
|
|
except (OSError, AttributeError):
|
|
pass
|
|
except (OSError, AttributeError):
|
|
pass
|
|
return attributes
|