Skip to content

Commit

Permalink
Refactor update osxmetadata (#804)
Browse files Browse the repository at this point in the history
* Updated osxmetadata to use v1.0.0

* Added README_DEV

* fix for missing detected_text xattr

* fix for missing detected_text xattr
  • Loading branch information
RhetTbull committed Oct 16, 2022
1 parent 0ba8bc3 commit 5665cf1
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 69 deletions.
4 changes: 2 additions & 2 deletions API_README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2204,9 +2204,9 @@ Attributes:

### <a name="textdetection">Text Detection</a>

The [PhotoInfo.detected_text()](#detected_text_method) and the `{detected_text}` template will perform text detection on the photos in your library. Text detection is a slow process so to avoid unnecessary re-processing of photos, osxphotos will cache the results of the text detection process as an extended attribute on the photo image file. Extended attributes do not modify the actual file. The extended attribute is named `osxphotos.metadata:detected_text` and can be viewed using the built-in [xattr](https://ss64.com/osx/xattr.html) command or my [osxmetadata](https://github.com/RhetTbull/osxmetadata) tool. If you want to remove the cached attribute, you can do so with osxmetadata as follows:
The [PhotoInfo.detected_text()](#detected_text_method) and the `{detected_text}` template will perform text detection on the photos in your library. Text detection is a slow process so to avoid unnecessary re-processing of photos, osxphotos will cache the results of the text detection process as an extended attribute on the photo image file. Extended attributes do not modify the actual file. The extended attribute is named `osxphotos.metadata:detected_text` and can be viewed using the built-in [xattr](https://ss64.com/osx/xattr.html) command or my [osxmetadata](https://github.com/RhetTbull/osxmetadata) tool. If you want to remove the cached attribute, you can do so with `xattr` as follows:

`osxmetadata --clear osxphotos.metadata:detected_text --walk ~/Pictures/Photos\ Library.photoslibrary/`
`find ~/Pictures/Photos\ Library.photoslibrary | xargs -I{} xattr -c osxphotos.metadata:detected_text '{}'`

### Utility Functions

Expand Down
25 changes: 25 additions & 0 deletions README_DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Developer Notes for osxphotos

These are notes for developers working on osxphotos. They're mostly to help me remember how to do things in this repo but will be useful to anyone who wants to contribute to osxphotos.

## Installing osxphotos

- Clone the repo: `git clone git@github.com:RhetTbull/osxphotos.git`
- Create a virtual environment and activate it: `python3 -m venv venv` then `source venv/bin/activate`. I use [pyenv](https://github.com/pyenv/pyenv) with [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) to manage my virtual environments
- Install the requirements: `pip install -r requirements.txt`
- Install the development requirements: `pip install -r requirements-dev.txt`
- Install osxphotos: `pip install -e .`

## Running tests

- Run all tests: `pytest`

See the [test README.md](tests/README.md) for more information on running tests.

## Building the package

- Run `./build.sh` to run the build script.

## Other Notes

[cogapp](https://nedbatchelder.com/code/cog/index.html) is used to update the README.md and other files. cog will be called from the build script as needed.
3 changes: 1 addition & 2 deletions osxphotos/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,9 @@
"description",
"findercomment",
"headline",
"keywords",
"participants",
"projects",
"rating",
"starrating",
"subject",
"title",
"version",
Expand Down
50 changes: 38 additions & 12 deletions osxphotos/cli/export.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
import subprocess
import sys
import time
from typing import Iterable, List, Tuple
from typing import Iterable, List, Optional, Tuple

import click
import osxmetadata
from osxmetadata import (
MDITEM_ATTRIBUTE_DATA,
MDITEM_ATTRIBUTE_SHORT_NAMES,
OSXMetaData,
Tag,
)
from osxmetadata.constants import _TAGS_NAMES

import osxphotos
from osxphotos._constants import (
Expand Down Expand Up @@ -86,7 +92,7 @@
from .help import ExportCommand, get_help_msg
from .list import _list_libraries
from .param_types import ExportDBType, FunctionCall, TemplateString
from .report_writer import report_writer_factory, ReportWriterNoOp
from .report_writer import ReportWriterNoOp, report_writer_factory
from .rich_progress import rich_progress
from .verbose import get_verbose_console, time_stamp, verbose_print

Expand Down Expand Up @@ -2683,9 +2689,9 @@ def write_finder_tags(
]
tags.extend(rendered_tags)

tags = [osxmetadata.Tag(tag) for tag in set(tags)]
tags = [Tag(tag, 0) for tag in set(tags)]
for f in files:
md = osxmetadata.OSXMetaData(f)
md = OSXMetaData(f)
if sorted(md.tags) != sorted(tags):
verbose_(f"Writing Finder tags to {f}")
md.tags = tags
Expand Down Expand Up @@ -2747,24 +2753,24 @@ def write_extended_attributes(
written = set()
skipped = set()
for f in files:
md = osxmetadata.OSXMetaData(f)
md = OSXMetaData(f)
for attr, value in attributes.items():
islist = osxmetadata.ATTRIBUTES[attr].list
attr_type = get_metadata_attribute_type(attr) or "str"
if value:
value = ", ".join(value) if not islist else sorted(value)
file_value = md.get_attribute(attr)
value = sorted(list(value)) if attr_type == "list" else ", ".join(value)
file_value = md.get(attr)

if file_value and islist:
if file_value and attr_type == "lists":
file_value = sorted(file_value)

if (not file_value and not value) or file_value == value:
# if both not set or both equal, nothing to do
# get_attribute returns None if not set and value will be [] if not set so can't directly compare
# get returns None if not set and value will be [] if not set so can't directly compare
verbose_(f"Skipping extended attribute {attr} for {f}: nothing to do")
skipped.add(f)
else:
verbose_(f"Writing extended attribute {attr} to {f}")
md.set_attribute(attr, value)
md.set(attr, value)
written.add(f)

return list(written), [f for f in skipped if f not in written]
Expand Down Expand Up @@ -2841,3 +2847,23 @@ def render_and_validate_report(report: str, exiftool_path: str, export_dir: str)
sys.exit(1)

return report


def get_metadata_attribute_type(attr: str) -> Optional[str]:
"""Get the type of a metadata attribute
Args:
attr: attribute name
Returns:
type of attribute as string or None if type is not known
"""
if attr in MDITEM_ATTRIBUTE_SHORT_NAMES:
attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr]
return (
"list"
if attr in _TAGS_NAMES
else MDITEM_ATTRIBUTE_DATA[attr]["python_type"]
if attr in MDITEM_ATTRIBUTE_DATA
else None
)
38 changes: 25 additions & 13 deletions osxphotos/cli/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import typing as t

import click
import osxmetadata
from osxmetadata import MDITEM_ATTRIBUTE_DATA, MDITEM_ATTRIBUTE_SHORT_NAMES
from rich.console import Console
from rich.markdown import Markdown

Expand Down Expand Up @@ -256,34 +256,46 @@ def get_help(self, ctx):
formatter.write_text(
"""
Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write
additional metadata to extended attributes in the file. These options will only work
if the destination filesystem supports extended attributes (most do).
additional metadata accessible by Spotlight to facilitate searching.
For example, --finder-tag-keyword writes all keywords (including any specified by '--keyword-template'
or other options) to Finder tags that are searchable in Spotlight using the syntax: 'tag:tagname'.
For example, if you have images with keyword "Travel" then using '--finder-tag-keywords' you could quickly
find those images in the Finder by typing 'tag:Travel' in the Spotlight search bar.
Finder tags are written to the 'com.apple.metadata:_kMDItemUserTags' extended attribute.
Unlike EXIF metadata, extended attributes do not modify the actual file. Most cloud storage services
do not synch extended attributes. Dropbox does sync them and any changes to a file's extended attributes
Unlike EXIF metadata, extended attributes do not modify the actual file;
the metadata is written to extended attributes associated with the file and the Spotlight metadata database.
Most cloud storage services do not synch extended attributes.
Dropbox does sync them and any changes to a file's extended attributes
will cause Dropbox to re-sync the files.
The following attributes may be used with '--xattr-template':
"""
)

# build help text from all the attribute names
# passed to click.HelpFormatter.write_dl for formatting
attr_tuples = [
(
rich_text("[bold]Attribute[/bold]", width=formatter.width),
rich_text("[bold]Description[/bold]", width=formatter.width),
),
*[
(
attr,
f"{osxmetadata.ATTRIBUTES[attr].help} ({osxmetadata.ATTRIBUTES[attr].constant})",
)
for attr in EXTENDED_ATTRIBUTE_NAMES
],
)
]
for attr_key in sorted(EXTENDED_ATTRIBUTE_NAMES):
# get short and long name
attr = MDITEM_ATTRIBUTE_SHORT_NAMES[attr_key]
short_name = MDITEM_ATTRIBUTE_DATA[attr]["short_name"]
long_name = MDITEM_ATTRIBUTE_DATA[attr]["name"]
constant = MDITEM_ATTRIBUTE_DATA[attr]["xattr_constant"]

# get help text
description = MDITEM_ATTRIBUTE_DATA[attr]["description"]
type_ = MDITEM_ATTRIBUTE_DATA[attr]["help_type"]
attr_help = f"{long_name}; {constant}; {description}; {type_}"

# add to list
attr_tuples.append((short_name, attr_help))

formatter.write_dl(attr_tuples)
formatter.write("\n")
formatter.write_text(
Expand Down
22 changes: 20 additions & 2 deletions osxphotos/photoinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -1440,11 +1440,29 @@ def _detected_text(self):
return []

md = OSXMetaData(path)
detected_text = md.get_attribute("osxphotos_detected_text")
try:

def decoder(val):
"""Decode value from JSON"""
return json.loads(val.decode("utf-8"))

detected_text = md.get_xattr(
"osxphotos.metadata:detected_text", decode=decoder
)
except KeyError:
detected_text = None
if detected_text is None:
orientation = self.orientation or None
detected_text = detect_text(path, orientation)
md.set_attribute("osxphotos_detected_text", detected_text)

def encoder(obj):
"""Encode value as JSON"""
val = json.dumps(obj)
return val.encode("utf-8")

md.set_xattr(
"osxphotos.metadata:detected_text", detected_text, encode=encoder
)
return detected_text

@property
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ bitmath>=1.3.3.1,<1.4.0.0
bpylist2==4.0.1
more-itertools>=8.8.0,<9.0.0
objexplore>=1.6.3,<2.0.0
osxmetadata>=0.99.34,<1.0.0
osxmetadata>=<1.0.0,<2.0.0
packaging>=21.3
pathvalidate>=2.4.1,<2.5.0
photoscript>=0.1.4,<0.2.0
photoscript>=0.1.6,<0.2.0
ptpython>=3.0.20,<3.1.0
pyobjc-core>=7.3,<9.0
pyobjc-framework-AVFoundation>=7.3,<9.0
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
"bpylist2==4.0.1",
"more-itertools>=8.8.0,<9.0.0",
"objexplore>=1.6.3,<2.0.0",
"osxmetadata>=0.99.34,<1.0.0",
"osxmetadata>=1.0.0,<2.0.0",
"packaging>=21.3",
"pathvalidate>=2.4.1,<3.0.0",
"photoscript>=0.1.4,<0.2.0",
"photoscript>=0.1.6,<0.2.0",
"ptpython>=3.0.20,<4.0.0",
"pyobjc-core>=7.3,<9.0",
"pyobjc-framework-AVFoundation>=7.3,<9.0",
Expand Down
Loading

0 comments on commit 5665cf1

Please sign in to comment.