Skip to content

Commit

Permalink
Implemented --auto-live, #1399
Browse files Browse the repository at this point in the history
  • Loading branch information
RhetTbull committed Apr 28, 2024
1 parent 33a96d2 commit b33a223
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 3 deletions.
53 changes: 51 additions & 2 deletions osxphotos/cli/import_cli.py
Expand Up @@ -22,6 +22,7 @@

import click
from cgmetadata import ImageMetadata
from makelive.makelive import make_live_photo
from rich.console import Console
from rich.markdown import Markdown
from strpdatetime import strpdatetime
Expand Down Expand Up @@ -106,6 +107,7 @@
IS_RAW_JPEG_PAIR = 4
IS_BURST_GROUP = 8
HAS_AAE_FILE = 16
AUTO_LIVE_PAIR = 32


def echo(message, emoji=True, **kwargs):
Expand Down Expand Up @@ -1253,6 +1255,7 @@ def import_files(
relative_to: pathlib.Path | None,
import_db: SQLiteKVStore,
verbose: Callable[..., None],
auto_live: bool,
):
"""Import files into Photos library
Expand Down Expand Up @@ -1286,10 +1289,25 @@ def import_files(
elif is_raw_pair(*file_tuple[:2]):
file_type |= IS_RAW_JPEG_PAIR
noun = "raw+jpeg pair"
elif auto_live and is_possible_live_pair(*file_tuple[:2]):
file_type |= AUTO_LIVE_PAIR
noun = "live photo pair"
verbose(
f"Converting to live photo pair: [filepath]{file_tuple[0]}[/], [filepath]{file_tuple[1]}[/]"
)
if not dry_run:
try:
makelive.make_live_photo(*file_tuple[:2])
except Exception as e:
echo(
f"Error converting {file_tuple[0]}, {file_tuple[1]} to live photo pair: {e}"
)
if has_aae(file_tuple):
file_type |= HAS_AAE_FILE
noun += " with .AAE file"
verbose(f"Processing {noun}: {', '.join([f.name for f in file_tuple])}")
verbose(
f"Processing {noun}: {', '.join([f'[filepath]{f.name}[/]' for f in file_tuple])}"
)
filepath = pathlib.Path(file_tuple[0]).resolve().absolute()
relative_filepath = get_relative_filepath(filepath, relative_to)

Expand All @@ -1315,7 +1333,11 @@ def import_files(
burst=bool(file_type & IS_BURST_GROUP),
burst_images=len(file_tuple) if file_type & IS_BURST_GROUP else 0,
live_photo=bool(file_type & IS_LIVE_PAIR),
live_video=str(file_tuple[1]) if file_type & IS_LIVE_PAIR else "",
live_video=(
str(file_tuple[1])
if (file_type & IS_LIVE_PAIR) or (file_type & AUTO_LIVE_PAIR)
else ""
),
raw_pair=bool(file_type & IS_RAW_JPEG_PAIR),
raw_image=str(file_tuple[1]) if file_type & IS_RAW_JPEG_PAIR else "",
aae_file=bool(file_type & HAS_AAE_FILE),
Expand Down Expand Up @@ -1911,6 +1933,21 @@ def get_help(self, ctx):
"XMP:Rating will be read from asset's metadata or from sidecar if --sidecar, --sidecare-filename is used. "
"Requires that exiftool be installed to read the rating from the asset's XMP data.",
)
@click.option(
"--auto-live",
"-E",
is_flag=True,
help="Automatically convert photo+video pairs into live images. "
"Live Photos (photo+video pair) exported from Photos contain a metadata content identifier that Photos "
"uses to associate the pair as a single Live Photo asset when re-imported. "
"Photo+video pairs taken on non-Apple devices will lack the content identifier and "
"thus will be imported as separate assets. "
"Use --auto-live to automatically convert these pairs to Live Photos upon import. "
"When --auto-live is used, a photo and a video with same base name, "
"for example 'IMG_1234.JPG' and 'IMG_1234.mov', in the same directory will be converted to Live Photos. "
"*NOTE*: Using this feature will modify the metadata in the files prior to import. "
"Ensure you have a backup of the original files if you want to preserve unmodified versions.",
)
@click.option(
"--parse-date",
"-P",
Expand Down Expand Up @@ -2196,6 +2233,7 @@ def import_main(
cli_obj: CLI_Obj,
album: tuple[str, ...],
append: bool,
auto_live: bool,
check: bool,
check_not: bool,
check_templates: bool,
Expand Down Expand Up @@ -2255,6 +2293,7 @@ def import_main(
def import_cli(
album: tuple[str, ...] = (),
append: bool = False,
auto_live: bool = False,
check: bool = False,
check_not: bool = False,
check_templates: bool = False,
Expand Down Expand Up @@ -2422,6 +2461,7 @@ def import_cli(
report_data=report_data,
relative_to=relative_to,
import_db=import_db,
auto_live=auto_live,
verbose=verbose,
)

Expand Down Expand Up @@ -2566,6 +2606,15 @@ def is_live_pair(filepath1: str | os.PathLike, filepath2: str | os.PathLike) ->
return makelive.is_live_photo_pair(filepath1, filepath2)


def is_possible_live_pair(
filepath1: str | os.PathLike, filepath2: str | os.PathLike
) -> bool:
"""Return True if photos could be a live photo pair (even if files lack the Content ID metadata"""
if is_image_file(filepath1) and is_video_file(filepath2):
return True
return False


def has_aae(filepaths: Iterable[str | os.PathLike]) -> bool:
"""Return True if any file in the list is an AAE file"""
for filepath in filepaths:
Expand Down
Binary file added tests/test-images/not_live.jpeg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/test-images/not_live.mov
Binary file not shown.
36 changes: 35 additions & 1 deletion tests/test_cli_import.py
Expand Up @@ -41,6 +41,8 @@
TEST_IMAGE_NO_EXIF = "tests/test-images/IMG_NO_EXIF.jpeg"
TEST_VIDEO_1 = "tests/test-images/Jellyfish.mov"
TEST_VIDEO_2 = "tests/test-images/IMG_0670B_NOGPS.MOV"
TEST_NOT_LIVE_PHOTO = "tests/test-images/not_live.jpeg"
TEST_NOT_LIVE_VIDEO = "tests/test-images/not_live.mov"

TEST_DATA = {
TEST_IMAGE_1: {
Expand Down Expand Up @@ -544,7 +546,6 @@ def test_import_exiftool_video_no_metadata():
)

assert result.exit_code == 0

import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image_1).name
uuid_1 = import_data[file_1]
Expand Down Expand Up @@ -1408,3 +1409,36 @@ def test_import_check_not():
)
assert result.exit_code == 0
assert "tests/test-images/IMG_3984.jpeg" in result.output


@pytest.mark.test_import
def test_import_auto_live(tmp_path):
"""Test import with --auto-live"""
cwd = os.getcwd()
test_image_1 = os.path.join(cwd, TEST_NOT_LIVE_PHOTO)
test_video_1 = os.path.join(cwd, TEST_NOT_LIVE_VIDEO)

# copy test files to tmp_path as they will be modified
shutil.copy(test_image_1, tmp_path)
shutil.copy(test_video_1, tmp_path)
test_image_1 = str(tmp_path / pathlib.Path(test_image_1).name)
test_video_1 = str(tmp_path / pathlib.Path(test_video_1).name)

runner = CliRunner()
result = runner.invoke(
import_main,
["--verbose", "--auto-live", test_image_1, test_video_1],
terminal_width=TERMINAL_WIDTH,
)

assert result.exit_code == 0
assert "Converting to live photo pair" in result.output

import_data = parse_import_output(result.output)
file_1 = pathlib.Path(test_image_1).name
uuid_1 = import_data[file_1]

# verify that the photo was imported as live photo
photosdb = PhotosDB()
photo = photosdb.query(QueryOptions(uuid=[uuid_1]))[0]
assert photo.live_photo

0 comments on commit b33a223

Please sign in to comment.