@@ -35,6 +35,7 @@ __all__ = [
|
|||||||
"DB_OPTION",
|
"DB_OPTION",
|
||||||
"DEBUG_OPTIONS",
|
"DEBUG_OPTIONS",
|
||||||
"DELETED_OPTIONS",
|
"DELETED_OPTIONS",
|
||||||
|
"FIELD_OPTION",
|
||||||
"JSON_OPTION",
|
"JSON_OPTION",
|
||||||
"QUERY_OPTIONS",
|
"QUERY_OPTIONS",
|
||||||
"THEME_OPTION",
|
"THEME_OPTION",
|
||||||
@@ -116,6 +117,19 @@ JSON_OPTION = click.option(
|
|||||||
help="Print output in JSON format.",
|
help="Print output in JSON format.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FIELD_OPTION = click.option(
|
||||||
|
"--field",
|
||||||
|
"-f",
|
||||||
|
metavar="FIELD TEMPLATE",
|
||||||
|
multiple=True,
|
||||||
|
nargs=2,
|
||||||
|
help="Output only specified custom fields. "
|
||||||
|
"FIELD is the name of the field and TEMPLATE is the template to use as the field value. "
|
||||||
|
"May be repeated to output multiple fields. "
|
||||||
|
"For example, to output photo uuid, name, and title: "
|
||||||
|
'`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def DELETED_OPTIONS(f):
|
def DELETED_OPTIONS(f):
|
||||||
o = click.option
|
o = click.option
|
||||||
|
|||||||
@@ -12,9 +12,16 @@ from osxphotos.phototemplate import RenderOptions
|
|||||||
from osxphotos.queryoptions import QueryOptions
|
from osxphotos.queryoptions import QueryOptions
|
||||||
|
|
||||||
from .color_themes import get_default_theme
|
from .color_themes import get_default_theme
|
||||||
from .common import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, JSON_OPTION, get_photos_db
|
from .common import (
|
||||||
|
DB_ARGUMENT,
|
||||||
|
DB_OPTION,
|
||||||
|
DELETED_OPTIONS,
|
||||||
|
FIELD_OPTION,
|
||||||
|
JSON_OPTION,
|
||||||
|
get_photos_db,
|
||||||
|
)
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
from .print_photo_info import print_photo_info
|
from .print_photo_info import print_photo_fields, print_photo_info
|
||||||
from .verbose import get_verbose_console
|
from .verbose import get_verbose_console
|
||||||
|
|
||||||
|
|
||||||
@@ -22,7 +29,7 @@ from .verbose import get_verbose_console
|
|||||||
@DB_OPTION
|
@DB_OPTION
|
||||||
@JSON_OPTION
|
@JSON_OPTION
|
||||||
@DELETED_OPTIONS
|
@DELETED_OPTIONS
|
||||||
@DB_ARGUMENT
|
@FIELD_OPTION
|
||||||
@click.option(
|
@click.option(
|
||||||
"--print",
|
"--print",
|
||||||
"print_template",
|
"print_template",
|
||||||
@@ -35,10 +42,19 @@ from .verbose import get_verbose_console
|
|||||||
"and only the rendered TEMPLATE values are printed. "
|
"and only the rendered TEMPLATE values are printed. "
|
||||||
"May be repeated to print multiple template strings. ",
|
"May be repeated to print multiple template strings. ",
|
||||||
)
|
)
|
||||||
|
@DB_ARGUMENT
|
||||||
@click.pass_obj
|
@click.pass_obj
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def dump(
|
def dump(
|
||||||
ctx, cli_obj, db, json_, deleted, deleted_only, photos_library, print_template
|
ctx,
|
||||||
|
cli_obj,
|
||||||
|
db,
|
||||||
|
deleted,
|
||||||
|
deleted_only,
|
||||||
|
field,
|
||||||
|
json_,
|
||||||
|
photos_library,
|
||||||
|
print_template,
|
||||||
):
|
):
|
||||||
"""Print list of all photos & associated info from the Photos library."""
|
"""Print list of all photos & associated info from the Photos library."""
|
||||||
|
|
||||||
@@ -71,22 +87,28 @@ def dump(
|
|||||||
if not deleted_only:
|
if not deleted_only:
|
||||||
photos += photosdb.photos(movies=True)
|
photos += photosdb.photos(movies=True)
|
||||||
|
|
||||||
if not print_template:
|
if not print_template and not field:
|
||||||
# just dump and be done
|
# just dump and be done
|
||||||
print_photo_info(photos, cli_json or json_)
|
print_photo_info(photos, cli_json or json_)
|
||||||
return
|
return
|
||||||
|
|
||||||
# have print template(s)
|
if field:
|
||||||
options = RenderOptions()
|
print_photo_fields(photos, field, cli_json or json_)
|
||||||
for p in photos:
|
|
||||||
for template in print_template:
|
if print_template:
|
||||||
rendered_templates, unmatched = p.render_template(
|
# have print template(s)
|
||||||
template,
|
options = RenderOptions()
|
||||||
options,
|
for p in photos:
|
||||||
)
|
for template in print_template:
|
||||||
if unmatched:
|
rendered_templates, unmatched = p.render_template(
|
||||||
rich_click_echo(f"[warning]Unmatched template field: {unmatched}[/]")
|
template,
|
||||||
for rendered_template in rendered_templates:
|
options,
|
||||||
if not rendered_template:
|
)
|
||||||
continue
|
if unmatched:
|
||||||
print(rendered_template)
|
rich_click_echo(
|
||||||
|
f"[warning]Unmatched template field: {unmatched}[/]"
|
||||||
|
)
|
||||||
|
for rendered_template in rendered_templates:
|
||||||
|
if not rendered_template:
|
||||||
|
continue
|
||||||
|
print(rendered_template)
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""print_photo_info function to print PhotoInfo objects"""
|
"""print_photo_info function to print PhotoInfo objects"""
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
import json
|
||||||
import sys
|
import sys
|
||||||
from typing import Callable, List
|
from typing import Callable, List, Tuple
|
||||||
|
|
||||||
from osxphotos.photoinfo import PhotoInfo
|
from osxphotos.photoinfo import PhotoInfo
|
||||||
|
|
||||||
@@ -110,3 +111,44 @@ def print_photo_info(
|
|||||||
)
|
)
|
||||||
for row in dump:
|
for row in dump:
|
||||||
csv_writer.writerow(row)
|
csv_writer.writerow(row)
|
||||||
|
|
||||||
|
|
||||||
|
def print_photo_fields(
|
||||||
|
photos: List[PhotoInfo], fields: Tuple[Tuple[str]], json_format: bool
|
||||||
|
):
|
||||||
|
"""Output custom field templates from PhotoInfo objects
|
||||||
|
|
||||||
|
Args:
|
||||||
|
photos: List of PhotoInfo objects
|
||||||
|
fields: Tuple of Tuple of field names/field templates to output"""
|
||||||
|
keys = [f[0] for f in fields]
|
||||||
|
data = []
|
||||||
|
for p in photos:
|
||||||
|
record = {}
|
||||||
|
for field in fields:
|
||||||
|
rendered_value, unmatched = p.render_template(field[1])
|
||||||
|
if unmatched:
|
||||||
|
raise ValueError(
|
||||||
|
f"Unmatched template variables in field {field[0]}: {field[1]}"
|
||||||
|
)
|
||||||
|
field_value = (
|
||||||
|
rendered_value[0]
|
||||||
|
if len(rendered_value) == 1
|
||||||
|
else ",".join(rendered_value)
|
||||||
|
if not json_format
|
||||||
|
else rendered_value
|
||||||
|
)
|
||||||
|
record[field[0]] = field_value
|
||||||
|
data.append(record)
|
||||||
|
|
||||||
|
if json_format:
|
||||||
|
print(json.dumps(data, indent=4))
|
||||||
|
else:
|
||||||
|
# dump as CSV
|
||||||
|
csv_writer = csv.writer(
|
||||||
|
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
|
||||||
|
)
|
||||||
|
# add headers
|
||||||
|
csv_writer.writerow(keys)
|
||||||
|
for record in data:
|
||||||
|
csv_writer.writerow(record.values())
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from .common import (
|
|||||||
DB_ARGUMENT,
|
DB_ARGUMENT,
|
||||||
DB_OPTION,
|
DB_OPTION,
|
||||||
DELETED_OPTIONS,
|
DELETED_OPTIONS,
|
||||||
|
FIELD_OPTION,
|
||||||
JSON_OPTION,
|
JSON_OPTION,
|
||||||
OSXPHOTOS_HIDDEN,
|
OSXPHOTOS_HIDDEN,
|
||||||
QUERY_OPTIONS,
|
QUERY_OPTIONS,
|
||||||
@@ -27,7 +28,7 @@ from .common import (
|
|||||||
load_uuid_from_file,
|
load_uuid_from_file,
|
||||||
)
|
)
|
||||||
from .list import _list_libraries
|
from .list import _list_libraries
|
||||||
from .print_photo_info import print_photo_info
|
from .print_photo_info import print_photo_fields, print_photo_info
|
||||||
from .verbose import get_verbose_console
|
from .verbose import get_verbose_console
|
||||||
|
|
||||||
|
|
||||||
@@ -76,6 +77,7 @@ from .verbose import get_verbose_console
|
|||||||
help="Quiet output; doesn't actually print query results. "
|
help="Quiet output; doesn't actually print query results. "
|
||||||
"Useful with --print and --add-to-album if you don't want to see the actual query results.",
|
"Useful with --print and --add-to-album if you don't want to see the actual query results.",
|
||||||
)
|
)
|
||||||
|
@FIELD_OPTION
|
||||||
@click.option(
|
@click.option(
|
||||||
"--print",
|
"--print",
|
||||||
"print_template",
|
"print_template",
|
||||||
@@ -113,6 +115,7 @@ def query(
|
|||||||
exif,
|
exif,
|
||||||
external_edit,
|
external_edit,
|
||||||
favorite,
|
favorite,
|
||||||
|
field,
|
||||||
folder,
|
folder,
|
||||||
from_date,
|
from_date,
|
||||||
from_time,
|
from_time,
|
||||||
@@ -399,6 +402,9 @@ def query(
|
|||||||
err=True,
|
err=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if field:
|
||||||
|
print_photo_fields(photos, field, cli_json or json_)
|
||||||
|
|
||||||
if print_template:
|
if print_template:
|
||||||
options = RenderOptions()
|
options = RenderOptions()
|
||||||
for p in photos:
|
for p in photos:
|
||||||
@@ -416,5 +422,5 @@ def query(
|
|||||||
continue
|
continue
|
||||||
print(rendered_template)
|
print(rendered_template)
|
||||||
|
|
||||||
if not quiet:
|
if not quiet and not field:
|
||||||
print_photo_info(photos, cli_json or json_, print_func=click.echo)
|
print_photo_info(photos, cli_json or json_, print_func=click.echo)
|
||||||
|
|||||||
@@ -8363,3 +8363,56 @@ def test_query_print_quiet():
|
|||||||
)
|
)
|
||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert result.output.strip() == f"uuid: {UUID_FAVORITE}"
|
assert result.output.strip() == f"uuid: {UUID_FAVORITE}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_field():
|
||||||
|
"""test query --field"""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
"--field",
|
||||||
|
"uuid",
|
||||||
|
"{uuid}",
|
||||||
|
"--field",
|
||||||
|
"name",
|
||||||
|
"{photo.original_filename}",
|
||||||
|
"--uuid",
|
||||||
|
UUID_FAVORITE,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert result.output.strip() == f"uuid,name\n{UUID_FAVORITE},{FILE_FAVORITE}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_field_json():
|
||||||
|
"""test query --field --json"""
|
||||||
|
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
query,
|
||||||
|
[
|
||||||
|
"--db",
|
||||||
|
os.path.join(cwd, PHOTOS_DB_15_7),
|
||||||
|
"--field",
|
||||||
|
"uuid",
|
||||||
|
"{uuid}",
|
||||||
|
"--field",
|
||||||
|
"name",
|
||||||
|
"{photo.original_filename}",
|
||||||
|
"--uuid",
|
||||||
|
UUID_FAVORITE,
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_results = json.loads(result.output)
|
||||||
|
assert json_results[0]["uuid"] == UUID_FAVORITE
|
||||||
|
assert json_results[0]["name"] == FILE_FAVORITE
|
||||||
|
|||||||
@@ -69,3 +69,57 @@ def test_dump_print(photos):
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
assert f"{photo.uuid}\t{photo.original_filename}" in result.output
|
assert f"{photo.uuid}\t{photo.original_filename}" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_dump_field(photos):
|
||||||
|
"""Test osxphotos dump --field"""
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
db_path = os.path.join(cwd, CLI_PHOTOS_DB)
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
dump,
|
||||||
|
[
|
||||||
|
"--db",
|
||||||
|
db_path,
|
||||||
|
"--deleted",
|
||||||
|
"--field",
|
||||||
|
"uuid",
|
||||||
|
"{uuid}",
|
||||||
|
"--field",
|
||||||
|
"name",
|
||||||
|
"{photo.original_filename}",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
for photo in photos:
|
||||||
|
assert f"{photo.uuid},{photo.original_filename}" in result.output
|
||||||
|
|
||||||
|
def test_dump_field_json(photos):
|
||||||
|
"""Test osxphotos dump --field --jso"""
|
||||||
|
runner = CliRunner()
|
||||||
|
cwd = os.getcwd()
|
||||||
|
db_path = os.path.join(cwd, CLI_PHOTOS_DB)
|
||||||
|
# pylint: disable=not-context-manager
|
||||||
|
with runner.isolated_filesystem():
|
||||||
|
result = runner.invoke(
|
||||||
|
dump,
|
||||||
|
[
|
||||||
|
"--db",
|
||||||
|
db_path,
|
||||||
|
"--deleted",
|
||||||
|
"--field",
|
||||||
|
"uuid",
|
||||||
|
"{uuid}",
|
||||||
|
"--field",
|
||||||
|
"name",
|
||||||
|
"{photo.original_filename}",
|
||||||
|
"--json",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert result.exit_code == 0
|
||||||
|
json_data = {record["uuid"]: record for record in json.loads(result.output)}
|
||||||
|
for photo in photos:
|
||||||
|
assert photo.uuid in json_data
|
||||||
|
assert json_data[photo.uuid]["name"] == photo.original_filename
|
||||||
|
|||||||
Reference in New Issue
Block a user