Add Apple target adapter for HEIC conversion and MOV processing

- Implemented `AppleTargetAdapter` to modify HEIC files for Apple compatibility and inject ContentIdentifier into MOV files.
- Created a base `TargetAdapter` class to define the interface for target adapters.
- Added utility script for converting Samsung HEIC motion photos into Apple-compatible Live Photo pairs.
- Introduced error handling for missing exiftool during MOV processing.
- Updated module structure to include target adapters for HEIC/Motion Photo outputs.
This commit is contained in:
Nymiro
2025-10-20 23:10:04 +13:00
parent 63f148951c
commit 9c480d9edb
25 changed files with 736 additions and 192 deletions
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env python3
"""
Utility script for converting Samsung HEIC motion photos into
Apple-compatible Live Photo pairs (HEIC + MOV) with matching ContentIdentifier.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
from pyheic_struct import AppleTargetAdapter, convert_motion_photo
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description=(
"Convert a Samsung HEIC motion photo into an Apple-compatible Live Photo "
"(HEIC + MOV) while keeping ContentIdentifier aligned."
),
)
parser.add_argument(
"source",
type=Path,
help="Path to the Samsung HEIC motion photo.",
)
parser.add_argument(
"-o",
"--output-dir",
type=Path,
default=None,
help="Optional directory for the generated files. Defaults to the source directory.",
)
parser.add_argument(
"--heic-name",
type=str,
default=None,
help="Optional filename (without directory) for the converted HEIC.",
)
parser.add_argument(
"--mov-name",
type=str,
default=None,
help="Optional filename (without directory) for the converted MOV.",
)
parser.add_argument(
"--skip-mov-tag",
action="store_true",
help="Skip writing ContentIdentifier into the MOV (no exiftool usage).",
)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
source_path = args.source.expanduser().resolve()
if not source_path.is_file():
print(f"Error: HEIC source file not found: {source_path}", file=sys.stderr)
return 1
output_dir = args.output_dir.expanduser().resolve() if args.output_dir else source_path.parent
output_dir.mkdir(parents=True, exist_ok=True)
if not args.skip_mov_tag and shutil.which("exiftool") is None:
print(
"Error: exiftool is required to inject ContentIdentifier into the MOV. "
"Install exiftool or use --skip-mov-tag.",
file=sys.stderr,
)
return 1
heic_filename = args.heic_name or f"{source_path.stem}_apple_compatible.HEIC"
mov_filename = args.mov_name or f"{source_path.stem}_apple_compatible.MOV"
heic_output = output_dir / heic_filename
mov_output = output_dir / mov_filename
print(f"Source HEIC: {source_path}")
print(f"Output HEIC: {heic_output}")
print(f"Output MOV: {mov_output}")
heic_path, mov_path = convert_motion_photo(
source_path,
vendor_hint="samsung",
target_adapter=AppleTargetAdapter(),
output_still=heic_output,
output_video=mov_output,
inject_content_id_into_mov=not args.skip_mov_tag,
)
print("Conversion finished successfully.")
print(f"HEIC saved to: {heic_path}")
if mov_path:
print(f"MOV saved to: {mov_path}")
else:
print("No MOV file generated (no embedded motion photo data found).")
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())