Skip to content

Commit

Permalink
Import of edited files now working
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed May 22, 2024
1 parent 4516421 commit b258ad7
Show file tree
Hide file tree
Showing 4 changed files with 72 additions and 41 deletions.
70 changes: 47 additions & 23 deletions osxphotos/cli/import_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,12 +709,12 @@ def get_help(self, ctx):
"--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. "
"This is used to associate sidecars to the edited version of a file when --sidecar or --sidecar-filename is used "
"and also to associate edited images to the original when importing adjustments exported with 'osxphotos export --export-aae'. "
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(),
)
Expand Down Expand Up @@ -961,10 +961,30 @@ def import_main(
- Photos are imported one at a time thus the "Imports" album in Photos will show
a new import group for each photo imported. Exception: Live photos (photo+video pair),
burst photos, and RAW+JPEG pairs will be imported together so that Photos processes
them correctly.
burst photos, edited photos, and RAW+JPEG pairs will be imported together so that
Photos processes them correctly.
- If there's an edited version of a photo along with the original, they will be imported as separate files, not as a single asset.
Edited Photos:
The import command will attempt to preserve adjustments to photos so that the imported asset
preserves the non-destructive edits. For this to work, there must be an associated .AAE file
for the photo. For example, these can be written using 'osxphotos export --export-aae'. If the
original file is named IMG_1234.jpg, the .AAE file should be named IMG_1234.aae or IMG_1234.AAE.
The edited version of the file must also be named following one of these two conventions:
Original: IMG_1234.jpg, edited: IMG_E1234.jpg
Original: IMG_1234.jpg, original: IMG_1234_edited.jpg
In the first form, the original is named with 3 letters, followed by underscore, followed by
4 digits and the edited has the same name with "E" in front of the 4 digits.
In the second form, a suffix is appended to the original name, in this example, "_edited", which
is the default suffix used by 'osxphotos export'. If you have used a different suffix, you can specify
it using '--edited-suffix SUFFIX'.
If edited files do not contain an associated .AAE or if they do not match one of these two conventions,
they will be imported as separate assets.
"""

kwargs = locals()
Expand Down Expand Up @@ -1121,12 +1141,6 @@ 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 or exportdb)):
rich_echo_error(
"[error] --dup-albums must be used with --skip-dups and --album"
Expand Down Expand Up @@ -1536,9 +1550,12 @@ def set_photo_metadata_from_exportdb(
dry_run: bool,
):
"""Set photo's metadata by reading metadata from exportdb"""
if photoinfo := export_db_get_photoinfo_for_filepath(
exportdb_path=exportdb_path, filepath=filepath, exiftool=exiftool_path
):
photoinfo = None
with suppress(ValueError):
photoinfo = export_db_get_photoinfo_for_filepath(
exportdb_path=exportdb_path, filepath=filepath, exiftool=exiftool_path
)
if photoinfo:
metadata = metadata_from_photoinfo(photoinfo)
verbose(
f"Setting metadata and location from export database for [filename]{filepath.name}[/]"
Expand Down Expand Up @@ -3062,7 +3079,7 @@ def import_files(
if file_type & FILE_TYPE_HAS_NON_APPLE_AAE:
file_tuple = strip_non_apple_aae_file(file_tuple, verbose)
verbose(
f"Processing {noun}: {', '.join([f'[filepath]{f.name}[/]' for f in file_tuple])}"
f"Processing {noun}: {', '.join([f'[filename]{f.name}[/]' for f in file_tuple])}"
)
filepath = pathlib.Path(file_tuple[0]).resolve().absolute()
relative_filepath = get_relative_filepath(filepath, relative_to)
Expand Down Expand Up @@ -3126,12 +3143,12 @@ def import_files(
if duplicates := fq.possible_duplicates(filepath):
# duplicate of file already in Photos library
verbose(
f"File [filepath]{filepath}[/] appears to be a duplicate of photos in the library: "
f"File [filename]{filepath.name}[/] appears to be a duplicate of photos in the library: "
f"{', '.join([f'[filename]{f}[/] ([uuid]{u}[/]) added [datetime]{d}[/] ' for u, d, f in duplicates])}"
)

if skip_dups:
verbose(f"Skipping duplicate [filepath]{filepath}[/]")
verbose(f"Skipping duplicate [filename]{filepath.name}[/]")
skipped_count += 1
report_record.imported = False

Expand Down Expand Up @@ -3171,14 +3188,15 @@ def import_files(
) as temp_dir:
if file_type & FILE_TYPE_SHOULD_STAGE_FILES:
verbose(
f"Staging files to {temp_dir} prior to import", level=2
f"Staging files to [filepath]{temp_dir}[/] prior to import",
level=2,
)
files_to_import = stage_files(file_tuple, temp_dir)
else:
files_to_import = file_tuple
if file_type & FILE_TYPE_AUTO_LIVE_PAIR:
verbose(
f"Converting to live photo pair: [filepath]{files_to_import[0].name}[/], [filepath]{files_to_import[1].name}[/]"
f"Converting to live photo pair: [filename]{files_to_import[0].name}[/], [filepath]{files_to_import[1].name}[/]"
)
try:
makelive.make_live_photo(*files_to_import[:2])
Expand All @@ -3189,7 +3207,7 @@ def import_files(
)
if file_type & FILE_TYPE_SHOULD_RENAME_EDITED:
verbose(
f"Renaming edited group {', '.join(f'[filepath]{f}[/]' for f in files_to_import)}",
f"Renaming edited group: {', '.join(f'[filename]{f.name}[/]' for f in files_to_import)}",
level=2,
)
files_to_import = rename_edited_group(
Expand All @@ -3200,6 +3218,10 @@ def import_files(
sidecar,
sidecar_filename_template,
)
verbose(
f"Edited group renamed: {', '.join(f'[filename]{f.name}[/]' for f in files_to_import)}",
level=2,
)
photo, error = import_photo_group(
files_to_import, dup_check, verbose
)
Expand Down Expand Up @@ -3590,8 +3612,6 @@ def stage_files(
return tuple(staged)


# ZZZ
@watch
def rename_edited_group(
filepaths: list[pathlib.Path],
edited_suffix: str | None,
Expand Down Expand Up @@ -3631,6 +3651,7 @@ def rename_edited_group(
if edited_regex.match(str(edited_file)):
return filepaths

# files are in a form that requires re-naming
counter_value = _increment_image_counter()

new_filepaths = []
Expand All @@ -3642,8 +3663,11 @@ def rename_edited_group(
filepath.rename(new_filepath)
new_filepaths.append(new_filepath)

# edited version needs to be renamed in format: IMG_0001_original_name.ext
original_stem = original_files[0].stem
edited_suffix = edited_file.suffix
new_edited_filepath = pathlib.Path(
edited_file.parent, f"IMG_E{counter_value}_{edited_file.name}"
edited_file.parent, f"IMG_E{counter_value}_{original_stem}{edited_suffix}"
)
edited_file.rename(new_edited_filepath)
new_filepaths.append(new_edited_filepath)
Expand Down
24 changes: 15 additions & 9 deletions osxphotos/export_db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import pathlib
import sqlite3
from typing import Any, Callable, Optional, Tuple, Union
import tenacity

import toml
from rich import print
Expand Down Expand Up @@ -620,13 +621,18 @@ def export_db_get_photoinfo_for_filepath(
if not exportdb_path:
raise ValueError(f"Could not find export database at path: {exportdb_path}")
exportdb = ExportDB(exportdb_path, last_export_dir)
if file_rec := exportdb.get_file_record(filepath):
if info_str := file_rec.photoinfo:
try:
info_dict = json.loads(info_str)
except Exception as e:
raise ValueError(f"Error loading PhotoInfo dict from database: {e}")
return photoinfo_from_dict(
info_dict, exiftool=str(exiftool) if exiftool else None
)
try:
# if the filepath is not in the export path, this will eventually fail with a RetryError
# as get_file_record keeps trying to read the database
if file_rec := exportdb.get_file_record(filepath):
if info_str := file_rec.photoinfo:
try:
info_dict = json.loads(info_str)
except Exception as e:
raise ValueError(f"Error loading PhotoInfo dict from database: {e}")
return photoinfo_from_dict(
info_dict, exiftool=str(exiftool) if exiftool else None
)
except tenacity.RetryError:
return None
return None
17 changes: 9 additions & 8 deletions tests/test_cli_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -1695,13 +1695,13 @@ def test_import_edited_renamed_with_aae(tmp_path):
source_image_edited = os.path.join(cwd, TEST_IMAGE_WITH_EDIT_EDITED)
source_image_aae = os.path.join(cwd, TEST_IMAGE_WITH_EDIT_AAE)

test_image_original = str(tmp_path / TEST_IMAGE_WITH_EDIT_ORIGINAL)
test_image_edited = str(tmp_path / TEST_IMAGE_WITH_EDIT_EDITED)
test_image_aae = str(tmp_path / TEST_IMAGE_WITH_EDIT_AAE)
shutil.copy(source_image_original, str(tmp_path))
shutil.copy(source_image_edited, str(tmp_path))
shutil.copy(source_image_aae, str(tmp_path))

shutil.copy(source_image_original, test_image_original)
shutil.copy(source_image_edited, test_image_edited)
shutil.copy(source_image_aae, test_image_aae)
test_image_original = str(tmp_path / pathlib.Path(TEST_IMAGE_WITH_EDIT_ORIGINAL).name)
test_image_edited = str(tmp_path / pathlib.Path(TEST_IMAGE_WITH_EDIT_EDITED).name)
test_image_aae = str(tmp_path / pathlib.Path(TEST_IMAGE_WITH_EDIT_AAE).name)

runner = CliRunner()
result = runner.invoke(
Expand All @@ -1713,10 +1713,11 @@ def test_import_edited_renamed_with_aae(tmp_path):
assert result.exit_code == 0
assert "Processing import group with .AAE file with edited version" in result.output
import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image_original).name

file_1 = f"IMG_0001_{pathlib.Path(TEST_IMAGE_WITH_EDIT_ORIGINAL).name}"
uuid_1 = import_data[file_1]

photosdb = PhotosDB()
photo = photosdb.query(QueryOptions(uuid=[uuid_1]))[0]
assert photo.hasadjustments
assert photo.original_filename == f"IMG_0001_{TEST_IMAGE_WITH_EDIT_ORIGINAL}"
assert photo.original_filename == file_1
2 changes: 1 addition & 1 deletion tests/test_cli_import_takeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def test_import_google_takeout(tmp_path):
conn.row_factory = sqlite3.Row
c = conn.cursor()
c.execute("SELECT COUNT(*) FROM report")
assert c.fetchone()[0] == 33
assert c.fetchone()[0] == 31

# test a photo that was imported
row = c.execute(
Expand Down

0 comments on commit b258ad7

Please sign in to comment.