This commit is contained in:
Rhet Turnbull 2021-12-25 05:41:37 -08:00
parent 7819740f70
commit debb17c952
4 changed files with 124 additions and 18 deletions

View File

@ -543,6 +543,17 @@ def QUERY_OPTIONS(f):
is_flag=True,
help="Filter for photos that are currently selected in Photos.",
),
o(
"--exif",
metavar="EXIF_TAG VALUE",
nargs=2,
multiple=True,
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
"To use --exif, exiftool must be installed and in the path.",
),
o(
"--query-eval",
metavar="CRITERIA",
@ -1190,6 +1201,7 @@ def export(
max_size,
regex,
selected,
exif,
query_eval,
query_function,
duplicate,
@ -1354,6 +1366,7 @@ def export(
max_size = cfg.max_size
regex = cfg.regex
selected = cfg.selected
exif = cfg.exif
query_eval = cfg.query_eval
query_function = cfg.query_function
duplicate = cfg.duplicate
@ -1673,6 +1686,7 @@ def export(
max_size=max_size,
regex=regex,
selected=selected,
exif=exif,
query_eval=query_eval,
function=query_function,
duplicate=duplicate,
@ -2085,6 +2099,7 @@ def query(
max_size,
regex,
selected,
exif,
query_eval,
query_function,
add_to_album,
@ -2120,6 +2135,7 @@ def query(
max_size,
regex,
selected,
exif,
duplicate,
]
exclusive = [
@ -2251,6 +2267,7 @@ def query(
function=query_function,
regex=regex,
selected=selected,
exif=exif,
duplicate=duplicate,
)
@ -2790,8 +2807,7 @@ def _render_suffix_template(
rendered_suffix, unmatched = photo.render_template(suffix_template, options)
except ValueError as e:
raise click.BadOptionUsage(
var_name,
f"Invalid template for {option_name} '{suffix_template}': {e}",
var_name, f"Invalid template for {option_name} '{suffix_template}': {e}",
)
if not rendered_suffix or unmatched:
raise click.BadOptionUsage(
@ -3481,12 +3497,7 @@ def write_finder_tags(
def write_extended_attributes(
photo,
files,
xattr_template,
strip=False,
export_dir=None,
export_db=None,
photo, files, xattr_template, strip=False, export_dir=None, export_db=None,
):
"""Writes extended attributes to exported files
@ -3606,7 +3617,7 @@ def run_post_command(
@cli.command()
@click.argument("packages", nargs=-1, required=True)
@click.option(
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
"-U", "--upgrade", is_flag=True, help="Upgrade packages to latest version"
)
def install(packages, upgrade):
"""Install Python packages into the same environment as osxphotos"""
@ -4072,9 +4083,7 @@ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMA
@cli.command(name="tutorial")
@click.argument(
"WIDTH",
nargs=-1,
type=click.INT,
"WIDTH", nargs=-1, type=click.INT,
)
@click.pass_obj
@click.pass_context

View File

@ -12,6 +12,7 @@ import re
import sys
import tempfile
from collections import OrderedDict
from collections.abc import Iterable
from datetime import datetime, timedelta, timezone
from pprint import pformat
from typing import List
@ -3470,6 +3471,34 @@ class PhotosDB:
# selection only works if photos selected in main media browser
photos = []
if options.exif:
matching_photos = []
for p in photos:
if not p.exiftool:
continue
exifdata = p.exiftool.asdict(normalized=True)
exifdata.update(p.exiftool.asdict(tag_groups=False, normalized=True))
for exiftag, exifvalue in options.exif:
if options.ignore_case:
exifvalue = exifvalue.lower()
exifdata_value = exifdata.get(exiftag.lower(), "")
if isinstance(exifdata_value, str):
exifdata_value = exifdata_value.lower()
elif isinstance(exifdata_value, Iterable):
exifdata_value = [v.lower() for v in exifdata_value]
else:
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
else:
exifdata_value = exifdata.get(exiftag.lower(), "")
if not isinstance(exifdata_value, (str, Iterable)):
exifdata_value = str(exifdata_value)
if exifvalue in exifdata_value:
matching_photos.append(p)
photos = matching_photos
if options.function:
for function in options.function:
photos = function[0](photos)

View File

@ -84,6 +84,7 @@ class QueryOptions:
no_location: Optional[bool] = None
function: Optional[List[Tuple[callable, str]]] = None
selected: Optional[bool] = None
exif: Optional[Iterable[Tuple[str, str]]] = None
def asdict(self):
return asdict(self)

View File

@ -886,6 +886,11 @@ EXPORT_UNICODE_TITLE_FILENAMES = [
"Frítest (3).jpg",
]
QUERY_EXIF_DATA = [("EXIF:Make", "FUJIFILM", ["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"])]
QUERY_EXIF_DATA_CASE_INSENSITIVE = [
("Make", "Fujifilm", ["6191423D-8DB8-4D4C-92BE-9BBBA308AAC4"])
]
def modify_file(filename):
"""appends data to a file to modify it"""
@ -1251,8 +1256,7 @@ def test_query_duplicate():
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--duplicate"],
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--duplicate"],
)
assert result.exit_code == 0
@ -1273,8 +1277,7 @@ def test_query_location():
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--location"],
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--location"],
)
assert result.exit_code == 0
@ -1296,8 +1299,7 @@ def test_query_no_location():
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--no-location"],
query, ["--json", "--db", os.path.join(cwd, CLI_PHOTOS_DB), "--no-location"],
)
assert result.exit_code == 0
@ -1308,6 +1310,71 @@ def test_query_no_location():
assert UUID_LOCATION not in uuid_got
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
@pytest.mark.parametrize("exiftag,exifvalue,uuid_expected", QUERY_EXIF_DATA)
def test_query_exif(exiftag, exifvalue, uuid_expected):
"""Test query with --exif"""
import json
import os
import os.path
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
"--exif",
exiftag,
exifvalue,
],
)
assert result.exit_code == 0
# build list of uuids we got from the output JSON
json_got = json.loads(result.output)
uuid_got = [photo["uuid"] for photo in json_got]
assert sorted(uuid_got) == sorted(uuid_expected)
@pytest.mark.skipif(exiftool is None, reason="exiftool not installed")
@pytest.mark.parametrize(
"exiftag,exifvalue,uuid_expected", QUERY_EXIF_DATA_CASE_INSENSITIVE
)
def test_query_exif_case_insensitive(exiftag, exifvalue, uuid_expected):
"""Test query with --exif -i"""
import json
import os
import os.path
from osxphotos.cli import query
runner = CliRunner()
cwd = os.getcwd()
result = runner.invoke(
query,
[
"--json",
"--db",
os.path.join(cwd, CLI_PHOTOS_DB),
"--exif",
exiftag,
exifvalue,
"-i",
],
)
assert result.exit_code == 0
# build list of uuids we got from the output JSON
json_got = json.loads(result.output)
uuid_got = [photo["uuid"] for photo in json_got]
assert sorted(uuid_got) == sorted(uuid_expected)
def test_export():
import glob
import os