Feature metadata changed 621 (#684)

* Added metadata_changed to ExportResults, #621

* Updated docs
This commit is contained in:
Rhet Turnbull
2022-05-06 21:05:59 -07:00
committed by GitHub
parent 95103f7c8d
commit 026e90a8ed
5 changed files with 32 additions and 15 deletions

View File

@@ -2981,6 +2981,9 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates. Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates.
#### `hexdigest`
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
#### `json()` #### `json()`
Returns a JSON representation of all photo info. Returns a JSON representation of all photo info.
@@ -4174,6 +4177,7 @@ Attributes:
* exiftool_error: list of errors generated by exiftool during export * exiftool_error: list of errors generated by exiftool during export
* xattr_written: list of files with extended attributes written during export * xattr_written: list of files with extended attributes written during export
* xattr_skipped: list of files where extended attributes were skipped when update=True * xattr_skipped: list of files where extended attributes were skipped when update=True
* metadata_changed: list of files where metadata changed since last export
* deleted_files: reserved for use by osxphotos CLI * deleted_files: reserved for use by osxphotos CLI
* deleted_directories: reserved for use by osxphotos CLI * deleted_directories: reserved for use by osxphotos CLI
* exported_album: reserved for use by osxphotos CLI * exported_album: reserved for use by osxphotos CLI

View File

@@ -50,6 +50,7 @@ def post_function(
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album # exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album # skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album # missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
# metadata_changed: list of filenames that had metadata changes since last export
for filename in results.exported: for filename in results.exported:
# do your processing here # do your processing here

View File

@@ -2,13 +2,11 @@
""" """
import dataclasses import dataclasses
import hashlib
import json import json
import logging import logging
import os import os
import pathlib import pathlib
import re import re
import tempfile
import typing as t import typing as t
from collections import namedtuple # pylint: disable=syntax-error from collections import namedtuple # pylint: disable=syntax-error
from dataclasses import asdict, dataclass from dataclasses import asdict, dataclass
@@ -45,14 +43,13 @@ from .photokit import (
from .phototemplate import RenderOptions from .phototemplate import RenderOptions
from .rich_utils import add_rich_markup_tag from .rich_utils import add_rich_markup_tag
from .uti import get_preferred_uti_extension from .uti import get_preferred_uti_extension
from .utils import increment_filename, lineno, list_directory from .utils import hexdigest, increment_filename, lineno, list_directory
__all__ = [ __all__ = [
"ExportError", "ExportError",
"ExportOptions", "ExportOptions",
"ExportResults", "ExportResults",
"PhotoExporter", "PhotoExporter",
"hexdigest",
"rename_jpeg_files", "rename_jpeg_files",
] ]
@@ -266,6 +263,7 @@ class ExportResults:
exported_album=None, exported_album=None,
skipped_album=None, skipped_album=None,
missing_album=None, missing_album=None,
metadata_changed=None,
): ):
self.exported = exported or [] self.exported = exported or []
self.new = new or [] self.new = new or []
@@ -292,6 +290,7 @@ class ExportResults:
self.exported_album = exported_album or [] self.exported_album = exported_album or []
self.skipped_album = skipped_album or [] self.skipped_album = skipped_album or []
self.missing_album = missing_album or [] self.missing_album = missing_album or []
self.metadata_changed = metadata_changed or []
def all_files(self): def all_files(self):
"""return all filenames contained in results""" """return all filenames contained in results"""
@@ -342,6 +341,7 @@ class ExportResults:
self.exported_album += other.exported_album self.exported_album += other.exported_album
self.skipped_album += other.skipped_album self.skipped_album += other.skipped_album
self.missing_album += other.missing_album self.missing_album += other.missing_album
self.metadata_changed += other.metadata_changed
return self return self
@@ -371,12 +371,14 @@ class ExportResults:
+ f",exported_album={self.exported_album}" + f",exported_album={self.exported_album}"
+ f",skipped_album={self.skipped_album}" + f",skipped_album={self.skipped_album}"
+ f",missing_album={self.missing_album}" + f",missing_album={self.missing_album}"
+ f",metadata_changed={self.metadata_changed}"
+ ")" + ")"
) )
class PhotoExporter: class PhotoExporter:
"""Export a photo""" """Export a photo"""
def __init__(self, photo: "PhotoInfo", tmpdir: t.Optional[str] = None): def __init__(self, photo: "PhotoInfo", tmpdir: t.Optional[str] = None):
self.photo = photo self.photo = photo
self._render_options = RenderOptions() self._render_options = RenderOptions()
@@ -739,7 +741,7 @@ class PhotoExporter:
return ShouldUpdate.EDITED_SIG_DIFFERENT return ShouldUpdate.EDITED_SIG_DIFFERENT
if options.force_update: if options.force_update:
current_digest = hexdigest(self.photo.json()) current_digest = self.photo.hexdigest
if current_digest != file_record.digest: if current_digest != file_record.digest:
# metadata in Photos changed, force update # metadata in Photos changed, force update
return ShouldUpdate.DIGEST_DIFFERENT return ShouldUpdate.DIGEST_DIFFERENT
@@ -1179,8 +1181,9 @@ class PhotoExporter:
rec.dest_sig = fileutil.file_sig(dest) rec.dest_sig = fileutil.file_sig(dest)
if options.exiftool: if options.exiftool:
rec.exifdata = self._exiftool_json_sidecar(options) rec.exifdata = self._exiftool_json_sidecar(options)
if options.force_update: if self.photo.hexdigest != rec.digest:
rec.digest = hexdigest(photoinfo) results.metadata_changed = [dest_str]
rec.digest = self.photo.hexdigest
return results return results
@@ -2011,13 +2014,6 @@ class PhotoExporter:
f.close() f.close()
def hexdigest(strval):
"""hexdigest of a string, using blake2b"""
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()
def _check_export_suffix(src, dest, edited): def _check_export_suffix(src, dest, edited):
"""Helper function for exporting photos to check file extensions of destination path. """Helper function for exporting photos to check file extensions of destination path.

View File

@@ -11,6 +11,7 @@ import os
import os.path import os.path
import pathlib import pathlib
from datetime import timedelta, timezone from datetime import timedelta, timezone
from functools import cached_property
from typing import Optional from typing import Optional
import yaml import yaml
@@ -54,7 +55,7 @@ from .scoreinfo import ScoreInfo
from .searchinfo import SearchInfo from .searchinfo import SearchInfo
from .text_detection import detect_text from .text_detection import detect_text
from .uti import get_preferred_uti_extension, get_uti_for_extension from .uti import get_preferred_uti_extension, get_uti_for_extension
from .utils import _get_resource_loc, list_directory from .utils import _get_resource_loc, hexdigest, list_directory
__all__ = ["PhotoInfo", "PhotoInfoNone"] __all__ = ["PhotoInfo", "PhotoInfoNone"]
@@ -1356,6 +1357,12 @@ class PhotoInfo:
self._exiftool = exiftool self._exiftool = exiftool
return self._exiftool return self._exiftool
@cached_property
def hexdigest(self):
"""Returns a unique digest of the photo's properties and metadata;
useful for detecting changes in any property/metadata of the photo"""
return hexdigest(self.json())
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD): def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
"""Detects text in photo and returns lists of results as (detected text, confidence) """Detects text in photo and returns lists of results as (detected text, confidence)

View File

@@ -3,6 +3,7 @@
import datetime import datetime
import fnmatch import fnmatch
import glob import glob
import hashlib
import importlib import importlib
import inspect import inspect
import logging import logging
@@ -29,6 +30,7 @@ __all__ = [
"expand_and_validate_filepath", "expand_and_validate_filepath",
"get_last_library_path", "get_last_library_path",
"get_system_library_path", "get_system_library_path",
"hexdigest",
"increment_filename_with_count", "increment_filename_with_count",
"increment_filename", "increment_filename",
"lineno", "lineno",
@@ -517,3 +519,10 @@ def get_latest_version() -> Tuple[Optional[str], str]:
def pluralize(count, singular, plural): def pluralize(count, singular, plural):
"""Return singular or plural based on count""" """Return singular or plural based on count"""
return singular if count == 1 else plural return singular if count == 1 else plural
def hexdigest(strval: str) -> str:
"""hexdigest of a string, using blake2b"""
h = hashlib.blake2b(digest_size=20)
h.update(bytes(strval, "utf-8"))
return h.hexdigest()