Skip to content

Commit

Permalink
Fixes #1470, apply sidecar to _edited photos, adds --edited-suffix
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Apr 27, 2024
1 parent 6a225ab commit 5fc2e17
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 74 deletions.
100 changes: 39 additions & 61 deletions osxphotos/cli/import_cli.py
Expand Up @@ -36,7 +36,7 @@

import osxphotos.sqlite3_datetime as sqlite3_datetime
from osxphotos._constants import (
_OSXPHOTOS_NONE_SENTINEL,
DEFAULT_EDITED_SUFFIX,
OSXPHOTOS_EXPORT_DB,
SQLITE_CHECK_SAME_THREAD,
)
Expand All @@ -45,6 +45,7 @@
from osxphotos.cli.common import get_data_dir
from osxphotos.cli.help import HELP_WIDTH
from osxphotos.cli.param_types import FunctionCall, StrpDateTimePattern, TemplateString
from osxphotos.cli.sidecar import get_sidecar_file_with_template
from osxphotos.datetime_utils import (
datetime_has_tz,
datetime_remove_tz,
Expand All @@ -60,7 +61,7 @@
metadata_from_sidecar,
)
from osxphotos.photoinfo import PhotoInfoNone
from osxphotos.photoinfo_file import PhotoInfoFromFile
from osxphotos.photoinfo_file import render_photo_template, strip_edited_suffix
from osxphotos.photosalbum import PhotosAlbumPhotoScript
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
from osxphotos.sqlite_utils import sqlite_columns
Expand Down Expand Up @@ -131,33 +132,6 @@ def echo(message, emoji=True, **kwargs):
# return wrapper


def get_sidecar_file(
filepath: pathlib.Path,
relative_filepath: pathlib.Path,
sidecar: bool,
sidecar_filename_template: str | None,
exiftool_path: str | None,
verbose: Callable[..., None],
) -> pathlib.Path | None:
sidecar_file = None
if sidecar or sidecar_filename_template:
if sidecar_filename_template:
if sidecar_files := render_photo_template(
filepath,
relative_filepath,
sidecar_filename_template,
exiftool_path,
None,
):
sidecar_file = pathlib.Path(sidecar_files[0])
else:
sidecar_file = get_sidecar_for_file(filepath)
if not sidecar_file or not sidecar_file.exists():
verbose(f"No sidecar found for [filepath]{filepath}[/]")
sidecar_file = None
return sidecar_file


def import_photo_group(
filepaths: tuple[pathlib.Path, ...], dup_check: bool, verbose: Callable[..., None]
) -> tuple[Photo | None, str | None]:
Expand Down Expand Up @@ -190,29 +164,6 @@ def import_photo_group(
return None, error_str


def render_photo_template(
filepath: pathlib.Path,
relative_filepath: pathlib.Path,
template: str,
exiftool_path: str | None,
sidecar: pathlib.Path | None,
):
"""Render template string for a photo"""
photoinfo = PhotoInfoFromFile(
filepath, exiftool=exiftool_path, sidecar=str(sidecar) if sidecar else None
)
options = RenderOptions(
none_str=_OSXPHOTOS_NONE_SENTINEL,
filepath=str(relative_filepath),
caller="import",
)
template_values, _ = photoinfo.render_template(template, options=options)
# filter out empty strings
template_values = [v.replace(_OSXPHOTOS_NONE_SENTINEL, "") for v in template_values]
template_values = [v for v in template_values if v]
return template_values


def add_photo_to_albums(
photo: Photo | None,
filepath: pathlib.Path,
Expand Down Expand Up @@ -624,7 +575,6 @@ def get_relative_filepath(
Raises: click.Abort if relative_to is not in the same path as filepath
"""
relative_filepath = filepath

# check relative_to here so we abort before import if relative_to is bad
if relative_to:
try:
Expand All @@ -635,7 +585,6 @@ def get_relative_filepath(
err=True,
)
raise click.Abort() from e

return relative_filepath


Expand All @@ -651,21 +600,25 @@ def check_templates_and_exit(
parse_date: str | None,
parse_folder_date: str | None,
sidecar: bool,
sidecar_filename_template: str | None = None,
sidecar_filename_template: str | None,
edited_suffix: str | None,
):
"""Renders templates against each file so user can verify correctness"""
for file in files:
file = pathlib.Path(file).absolute().resolve()
relative_filepath = get_relative_filepath(file, relative_to)
sidecar_file = get_sidecar_file(
sidecar_file = get_sidecar_file_with_template(
filepath=file,
relative_filepath=relative_filepath,
sidecar=sidecar,
sidecar_filename_template=sidecar_filename_template,
edited_suffix=edited_suffix,
exiftool_path=exiftool_path,
verbose=echo,
)
echo(f"[filepath]{file}[/]:")
if sidecar and not sidecar_file:
echo("no sidecar file found")
else:
echo(f"sidecar file: {sidecar_file}")
if exiftool:
metadata = metadata_from_file(file, exiftool_path)
echo(f"exiftool title: {metadata.title}")
Expand Down Expand Up @@ -1242,6 +1195,7 @@ def import_files(
resume: bool,
clear_metadata: bool,
clear_location: bool,
edited_suffix: str | None,
exiftool: bool,
exiftool_path: str,
sidecar: bool,
Expand Down Expand Up @@ -1335,14 +1289,15 @@ def import_files(
report_record = report_data[filepath]

if sidecar or sidecar_filename_template:
sidecar_file = get_sidecar_file(
sidecar_file = get_sidecar_file_with_template(
filepath=filepath,
relative_filepath=relative_filepath,
sidecar=sidecar,
sidecar_filename_template=sidecar_filename_template,
edited_suffix=edited_suffix,
exiftool_path=exiftool_path,
verbose=verbose,
)
if not sidecar_file:
verbose(f"No sidecar file found for [filepath]{filepath}[/]")
else:
sidecar_file = None

Expand Down Expand Up @@ -2007,6 +1962,19 @@ def get_help(self, ctx):
"See also --sidecar-ignore-date. "
"Note: --sidecar and --sidecar-filename are mutually exclusive.",
)
@click.option(
"--edited-suffix",
metavar="TEMPLATE",
help="Optional suffix template used for naming edited photos. "
"This is used to associate sidecars to the edited version of a file when --sidecar or --sidecar-filename is used. "
f"By default, osxphotos will look for edited photos using default 'osxphotos export' suffix of '{DEFAULT_EDITED_SUFFIX}' "
"If your edited photos have a different suffix you can use '--edited-suffix' to specify the suffix. "
"For example, with '--edited-suffix _bearbeiten', the import command will look for a file named 'photoname_bearbeiten.ext' "
"and associated that with a sidecar named 'photoname.xmp', etc. "
"The --edited-suffix option is only valid when used with --sidecar or --sidecar-filename. "
"Multi-value templates (see Templating System in the OSXPhotos docs) are not permitted with --edited-suffix.",
type=TemplateString(),
)
@click.option(
"--sidecar-ignore-date",
"-i",
Expand Down Expand Up @@ -2182,6 +2150,7 @@ def import_main(
dry_run: bool,
dup_albums: bool,
dup_check: bool,
edited_suffix: str | None,
exiftool: bool,
exiftool_path: str | None,
files: tuple[str, ...],
Expand Down Expand Up @@ -2239,6 +2208,7 @@ def import_cli(
dry_run: bool = False,
dup_albums: bool = False,
dup_check: bool = False,
edited_suffix: str | None = None,
exiftool: bool = False,
exiftool_path: str | None = None,
files: tuple[str, ...] = (),
Expand Down Expand Up @@ -2298,6 +2268,7 @@ def import_cli(
parse_folder_date=parse_folder_date,
sidecar=sidecar,
sidecar_filename_template=sidecar_filename_template,
edited_suffix=edited_suffix,
)

files_to_import = group_files_to_import(files)
Expand Down Expand Up @@ -2339,6 +2310,12 @@ def import_cli(
)
raise click.Abort()

if edited_suffix and not (sidecar or sidecar_filename_template):
rich_echo_error(
"[error] --edited-suffix must be used with --sidecar or --sidecar-filename"
)
raise click.Abort()

if dup_albums and not (skip_dups and album):
rich_echo_error(
"[error] --dup-albums must be used with --skip-dups and --album"
Expand All @@ -2364,6 +2341,7 @@ def import_cli(
resume=resume,
clear_metadata=clear_metadata,
clear_location=clear_location,
edited_suffix=edited_suffix,
exiftool=exiftool,
exiftool_path=exiftool_path,
sidecar=sidecar,
Expand Down
60 changes: 59 additions & 1 deletion osxphotos/cli/sidecar.py
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import logging
import pathlib
from functools import cache
from typing import Callable
Expand All @@ -11,9 +12,17 @@

from osxphotos.cli.click_rich_echo import rich_echo_error
from osxphotos.exportoptions import ExportResults
from osxphotos.metadata_reader import get_sidecar_for_file
from osxphotos.photoinfo import PhotoInfo
from osxphotos.photoinfo_file import (
PhotoInfoFromFile,
render_photo_template,
strip_edited_suffix,
)
from osxphotos.phototemplate import PhotoTemplate, RenderOptions

logger = logging.getLogger("osxphotos")


@cache
def get_template(template: str) -> Template:
Expand Down Expand Up @@ -109,7 +118,6 @@ def generate_user_sidecar(
else:
sidecar_results.sidecar_user_written.append(template_filename)

print(sidecar_results)
return sidecar_results


Expand Down Expand Up @@ -179,3 +187,53 @@ def _render_sidecar_and_write_data(
f.write(sidecar_data)

return None


def get_sidecar_file_with_template(
filepath: pathlib.Path,
sidecar: bool,
sidecar_filename_template: str | None,
edited_suffix: str | None,
exiftool_path: str | None,
) -> pathlib.Path | None:
"""Find sidecar file for photo with optional template for the sidecar and/or edited suffix"""
if not (sidecar or sidecar_filename_template):
return None
sidecar_file = None
if sidecar_filename_template:
if sidecars := render_photo_template(
filepath,
None,
sidecar_filename_template,
exiftool_path,
None,
):
# allow multiple values to be rendered and checked
# but only one will be used if more than one is valid
for f in sidecars:
sidecar_file = pathlib.Path(f)
if sidecar_file.exists():
break
else:
sidecar_file = None
else:
logger.warning(
f"Could not render sidecar template '{sidecar_filename_template}' for '{filepath}'"
)
else:
sidecar_file = get_sidecar_for_file(filepath)
if not sidecar_file or not sidecar_file.exists():
if edited_suffix:
# try again with the edited suffix removed
filepath = strip_edited_suffix(
filepath, edited_suffix, exiftool_path
)
return get_sidecar_file_with_template(
filepath,
sidecar,
sidecar_filename_template,
None,
exiftool_path,
)
return None
return sidecar_file
15 changes: 8 additions & 7 deletions osxphotos/metadata_reader.py
Expand Up @@ -8,7 +8,7 @@
import re
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional, Tuple
from typing import Callable, Optional, Tuple

from .datetime_utils import (
datetime_has_tz,
Expand Down Expand Up @@ -121,11 +121,11 @@ def get_sidecar_for_file(filepath: str | pathlib.Path) -> pathlib.Path | None:
filepath = (
pathlib.Path(filepath) if not isinstance(filepath, pathlib.Path) else filepath
)
for ext in ["json", "xmp"]:
sidecar = pathlib.Path(f"{filepath}.{ext}")
for ext in [".json", ".xmp"]:
sidecar = pathlib.Path(f"{filepath}{ext}")
if sidecar.is_file():
return sidecar
sidecar = filepath.with_suffix("." + ext)
sidecar = filepath.with_suffix(ext)
if sidecar.is_file():
return sidecar

Expand All @@ -135,13 +135,14 @@ def get_sidecar_for_file(filepath: str | pathlib.Path) -> pathlib.Path | None:
# in form img_1234(1).jpg but the sidecar may be named img_1234.jpg(1).json

stem = filepath.stem
if stem.endswith("-edited"):
# strip off -edited suffix
# strip off -edited suffix (Google Takeout) or _edited (OSXPhotos edited images with default suffix)
if stem.endswith("-edited") or stem.endswith("_edited"):
# strip off -edited/_edited suffix
stem = stem[:-7]
new_filepath = filepath.with_stem(stem)
return get_sidecar_for_file(new_filepath)

# strip off (1) suffix
# strip off (1) suffix for Google takeout naming scheme
if match := re.match(r"(.*)(\(\d+\))$", stem):
stem = match.groups()[0]
new_filepath = pathlib.Path(
Expand Down

0 comments on commit 5fc2e17

Please sign in to comment.