Refactored photoinfo, photoexporter; #462
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
from ._constants import AlbumSortOrder
|
from ._constants import AlbumSortOrder
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
from .exiftool import ExifTool
|
from .exiftool import ExifTool
|
||||||
from .photoinfo import ExportResults, PhotoInfo
|
from .photoexporter import ExportResults, PhotoExporter
|
||||||
|
from .photoinfo import PhotoInfo
|
||||||
from .photosdb import PhotosDB
|
from .photosdb import PhotosDB
|
||||||
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
from .photosdb._photosdb_process_comments import CommentInfo, LikeInfo
|
||||||
from .phototemplate import PhotoTemplate
|
from .phototemplate import PhotoTemplate
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import shlex
|
|||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
from runpy import run_module
|
||||||
|
|
||||||
import bitmath
|
import bitmath
|
||||||
import click
|
import click
|
||||||
@@ -20,7 +21,6 @@ import photoscript
|
|||||||
import rich.traceback
|
import rich.traceback
|
||||||
import yaml
|
import yaml
|
||||||
from rich import pretty
|
from rich import pretty
|
||||||
from runpy import run_module
|
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
@@ -56,7 +56,8 @@ from .exiftool import get_exiftool_path
|
|||||||
from .export_db import ExportDB, ExportDBInMemory
|
from .export_db import ExportDB, ExportDBInMemory
|
||||||
from .fileutil import FileUtil, FileUtilNoOp
|
from .fileutil import FileUtil, FileUtilNoOp
|
||||||
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
from .path_utils import is_valid_filepath, sanitize_filename, sanitize_filepath
|
||||||
from .photoinfo import ExportResults, PhotoInfo
|
from .photoexporter import ExportResults, PhotoExporter
|
||||||
|
from .photoinfo import PhotoInfo
|
||||||
from .photokit import check_photokit_authorization, request_photokit_authorization
|
from .photokit import check_photokit_authorization, request_photokit_authorization
|
||||||
from .photosalbum import PhotosAlbum
|
from .photosalbum import PhotosAlbum
|
||||||
from .phototemplate import PhotoTemplate, RenderOptions
|
from .phototemplate import PhotoTemplate, RenderOptions
|
||||||
@@ -2954,7 +2955,8 @@ def export_photo_to_directory(
|
|||||||
tries += 1
|
tries += 1
|
||||||
error = 0
|
error = 0
|
||||||
try:
|
try:
|
||||||
export_results = photo.export2(
|
exporter = PhotoExporter(photo)
|
||||||
|
export_results = exporter.export2(
|
||||||
dest_path,
|
dest_path,
|
||||||
original_filename=filename,
|
original_filename=filename,
|
||||||
edited=edited,
|
edited=edited,
|
||||||
@@ -3466,7 +3468,7 @@ def write_finder_tags(
|
|||||||
skipped = []
|
skipped = []
|
||||||
if keywords:
|
if keywords:
|
||||||
# match whatever keywords would've been used in --exiftool or --sidecar
|
# match whatever keywords would've been used in --exiftool or --sidecar
|
||||||
exif = photo._exiftool_dict(
|
exif = PhotoExporter(photo)._exiftool_dict(
|
||||||
use_albums_as_keywords=album_keyword,
|
use_albums_as_keywords=album_keyword,
|
||||||
use_persons_as_keywords=person_keyword,
|
use_persons_as_keywords=person_keyword,
|
||||||
keyword_template=keyword_template,
|
keyword_template=keyword_template,
|
||||||
|
|||||||
28
osxphotos/exifinfo.py
Normal file
28
osxphotos/exifinfo.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
""" ExifInfo class to expose EXIF info from the library """
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ExifInfo:
|
||||||
|
"""EXIF info associated with a photo from the Photos library"""
|
||||||
|
|
||||||
|
flash_fired: bool
|
||||||
|
iso: int
|
||||||
|
metering_mode: int
|
||||||
|
sample_rate: int
|
||||||
|
track_format: int
|
||||||
|
white_balance: int
|
||||||
|
aperture: float
|
||||||
|
bit_rate: float
|
||||||
|
duration: float
|
||||||
|
exposure_bias: float
|
||||||
|
focal_length: float
|
||||||
|
fps: float
|
||||||
|
latitude: float
|
||||||
|
longitude: float
|
||||||
|
shutter_speed: float
|
||||||
|
camera_make: str
|
||||||
|
camera_model: str
|
||||||
|
codec: str
|
||||||
|
lens_model: str
|
||||||
2179
osxphotos/photoexporter.py
Normal file
2179
osxphotos/photoexporter.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,7 @@ from typing import Optional
|
|||||||
import yaml
|
import yaml
|
||||||
from osxmetadata import OSXMetaData
|
from osxmetadata import OSXMetaData
|
||||||
|
|
||||||
from .._constants import (
|
from ._constants import (
|
||||||
_MOVIE_TYPE,
|
_MOVIE_TYPE,
|
||||||
_PHOTO_TYPE,
|
_PHOTO_TYPE,
|
||||||
_PHOTOS_4_ALBUM_KIND,
|
_PHOTOS_4_ALBUM_KIND,
|
||||||
@@ -37,16 +37,21 @@ from .._constants import (
|
|||||||
BURST_SELECTED,
|
BURST_SELECTED,
|
||||||
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
TEXT_DETECTION_CONFIDENCE_THRESHOLD,
|
||||||
)
|
)
|
||||||
from ..adjustmentsinfo import AdjustmentsInfo
|
from .adjustmentsinfo import AdjustmentsInfo
|
||||||
from ..albuminfo import AlbumInfo, ImportInfo, ProjectInfo
|
from .albuminfo import AlbumInfo, ImportInfo, ProjectInfo
|
||||||
from ..momentinfo import MomentInfo
|
from .exifinfo import ExifInfo
|
||||||
from ..personinfo import FaceInfo, PersonInfo
|
from .exiftool import ExifToolCaching, get_exiftool_path
|
||||||
from ..phototemplate import PhotoTemplate, RenderOptions
|
from .momentinfo import MomentInfo
|
||||||
from ..placeinfo import PlaceInfo4, PlaceInfo5
|
from .personinfo import FaceInfo, PersonInfo
|
||||||
from ..query_builder import get_query
|
from .photoexporter import PhotoExporter
|
||||||
from ..text_detection import detect_text
|
from .phototemplate import PhotoTemplate, RenderOptions
|
||||||
from ..uti import get_preferred_uti_extension, get_uti_for_extension
|
from .placeinfo import PlaceInfo4, PlaceInfo5
|
||||||
from ..utils import _debug, _get_resource_loc, findfiles
|
from .query_builder import get_query
|
||||||
|
from .scoreinfo import ScoreInfo
|
||||||
|
from .searchinfo import SearchInfo
|
||||||
|
from .text_detection import detect_text
|
||||||
|
from .uti import get_preferred_uti_extension, get_uti_for_extension
|
||||||
|
from .utils import _debug, _get_resource_loc, findfiles
|
||||||
|
|
||||||
|
|
||||||
class PhotoInfo:
|
class PhotoInfo:
|
||||||
@@ -55,42 +60,12 @@ class PhotoInfo:
|
|||||||
including keywords, persons, albums, uuid, path, etc.
|
including keywords, persons, albums, uuid, path, etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# import additional methods
|
|
||||||
from ._photoinfo_comments import comments, likes
|
|
||||||
from ._photoinfo_exifinfo import ExifInfo, exif_info
|
|
||||||
from ._photoinfo_exiftool import exiftool
|
|
||||||
from ._photoinfo_export import (
|
|
||||||
ExportResults,
|
|
||||||
_exiftool_dict,
|
|
||||||
_exiftool_json_sidecar,
|
|
||||||
_export_photo,
|
|
||||||
_export_photo_with_photos_export,
|
|
||||||
_get_exif_keywords,
|
|
||||||
_get_exif_persons,
|
|
||||||
_write_exif_data,
|
|
||||||
_write_sidecar,
|
|
||||||
_xmp_sidecar,
|
|
||||||
export,
|
|
||||||
export2,
|
|
||||||
)
|
|
||||||
from ._photoinfo_scoreinfo import ScoreInfo, score
|
|
||||||
from ._photoinfo_searchinfo import (
|
|
||||||
SearchInfo,
|
|
||||||
labels,
|
|
||||||
labels_normalized,
|
|
||||||
search_info,
|
|
||||||
search_info_normalized,
|
|
||||||
)
|
|
||||||
|
|
||||||
def __init__(self, db=None, uuid=None, info=None):
|
def __init__(self, db=None, uuid=None, info=None):
|
||||||
self._uuid = uuid
|
self._uuid = uuid
|
||||||
self._info = info
|
self._info = info
|
||||||
self._db = db
|
self._db = db
|
||||||
self._verbose = self._db._verbose
|
self._verbose = self._db._verbose
|
||||||
|
|
||||||
# TODO: remove this once refactor of PhotoExporter is done
|
|
||||||
self._render_options = RenderOptions()
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def filename(self):
|
def filename(self):
|
||||||
"""filename of the picture"""
|
"""filename of the picture"""
|
||||||
@@ -1140,21 +1115,239 @@ class PhotoInfo:
|
|||||||
self._owner = None
|
self._owner = None
|
||||||
return self._owner
|
return self._owner
|
||||||
|
|
||||||
def render_template(
|
@property
|
||||||
self, template_str: str, options: Optional[RenderOptions] = None
|
def score(self):
|
||||||
):
|
"""Computed score information for a photo
|
||||||
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
|
||||||
|
|
||||||
Args:
|
|
||||||
template_str: a template string with fields to render
|
|
||||||
options: a RenderOptions instance
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
ScoreInfo instance
|
||||||
"""
|
"""
|
||||||
options = options or RenderOptions()
|
|
||||||
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
return template.render(template_str, options)
|
logging.debug(f"score not implemented for this database version")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
return self._scoreinfo # pylint: disable=access-member-before-definition
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=scores["overall_aesthetic"],
|
||||||
|
curation=scores["curation"],
|
||||||
|
promotion=scores["promotion"],
|
||||||
|
highlight_visibility=scores["highlight_visibility"],
|
||||||
|
behavioral=scores["behavioral"],
|
||||||
|
failure=scores["failure"],
|
||||||
|
harmonious_color=scores["harmonious_color"],
|
||||||
|
immersiveness=scores["immersiveness"],
|
||||||
|
interaction=scores["interaction"],
|
||||||
|
interesting_subject=scores["interesting_subject"],
|
||||||
|
intrusive_object_presence=scores["intrusive_object_presence"],
|
||||||
|
lively_color=scores["lively_color"],
|
||||||
|
low_light=scores["low_light"],
|
||||||
|
noise=scores["noise"],
|
||||||
|
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
||||||
|
pleasant_composition=scores["pleasant_composition"],
|
||||||
|
pleasant_lighting=scores["pleasant_lighting"],
|
||||||
|
pleasant_pattern=scores["pleasant_pattern"],
|
||||||
|
pleasant_perspective=scores["pleasant_perspective"],
|
||||||
|
pleasant_post_processing=scores["pleasant_post_processing"],
|
||||||
|
pleasant_reflection=scores["pleasant_reflection"],
|
||||||
|
pleasant_symmetry=scores["pleasant_symmetry"],
|
||||||
|
sharply_focused_subject=scores["sharply_focused_subject"],
|
||||||
|
tastefully_blurred=scores["tastefully_blurred"],
|
||||||
|
well_chosen_subject=scores["well_chosen_subject"],
|
||||||
|
well_framed_subject=scores["well_framed_subject"],
|
||||||
|
well_timed_shot=scores["well_timed_shot"],
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
|
except KeyError:
|
||||||
|
self._scoreinfo = ScoreInfo(
|
||||||
|
overall=0.0,
|
||||||
|
curation=0.0,
|
||||||
|
promotion=0.0,
|
||||||
|
highlight_visibility=0.0,
|
||||||
|
behavioral=0.0,
|
||||||
|
failure=0.0,
|
||||||
|
harmonious_color=0.0,
|
||||||
|
immersiveness=0.0,
|
||||||
|
interaction=0.0,
|
||||||
|
interesting_subject=0.0,
|
||||||
|
intrusive_object_presence=0.0,
|
||||||
|
lively_color=0.0,
|
||||||
|
low_light=0.0,
|
||||||
|
noise=0.0,
|
||||||
|
pleasant_camera_tilt=0.0,
|
||||||
|
pleasant_composition=0.0,
|
||||||
|
pleasant_lighting=0.0,
|
||||||
|
pleasant_pattern=0.0,
|
||||||
|
pleasant_perspective=0.0,
|
||||||
|
pleasant_post_processing=0.0,
|
||||||
|
pleasant_reflection=0.0,
|
||||||
|
pleasant_symmetry=0.0,
|
||||||
|
sharply_focused_subject=0.0,
|
||||||
|
tastefully_blurred=0.0,
|
||||||
|
well_chosen_subject=0.0,
|
||||||
|
well_framed_subject=0.0,
|
||||||
|
well_timed_shot=0.0,
|
||||||
|
)
|
||||||
|
return self._scoreinfo
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_info(self):
|
||||||
|
"""returns SearchInfo object for photo
|
||||||
|
only valid on Photos 5, on older libraries, returns None
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# memoize SearchInfo object
|
||||||
|
try:
|
||||||
|
return self._search_info
|
||||||
|
except AttributeError:
|
||||||
|
self._search_info = SearchInfo(self)
|
||||||
|
return self._search_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_info_normalized(self):
|
||||||
|
"""returns SearchInfo object for photo that produces normalized results
|
||||||
|
only valid on Photos 5, on older libraries, returns None
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# memoize SearchInfo object
|
||||||
|
try:
|
||||||
|
return self._search_info_normalized
|
||||||
|
except AttributeError:
|
||||||
|
self._search_info_normalized = SearchInfo(self, normalized=True)
|
||||||
|
return self._search_info_normalized
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels(self):
|
||||||
|
"""returns list of labels applied to photo by Photos image categorization
|
||||||
|
only valid on Photos 5, on older libraries returns empty list
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.search_info.labels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels_normalized(self):
|
||||||
|
"""returns normalized list of labels applied to photo by Photos image categorization
|
||||||
|
only valid on Photos 5, on older libraries returns empty list
|
||||||
|
"""
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return self.search_info_normalized.labels
|
||||||
|
|
||||||
|
@property
|
||||||
|
def comments(self):
|
||||||
|
"""Returns list of Comment objects for any comments on the photo (sorted by date)"""
|
||||||
|
try:
|
||||||
|
return self._db._db_comments_uuid[self.uuid]["comments"]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def likes(self):
|
||||||
|
"""Returns list of Like objects for any likes on the photo (sorted by date)"""
|
||||||
|
try:
|
||||||
|
return self._db._db_comments_uuid[self.uuid]["likes"]
|
||||||
|
except:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exif_info(self):
|
||||||
|
"""Returns an ExifInfo object with the EXIF data for photo
|
||||||
|
Note: the returned EXIF data is the data Photos stores in the database on import;
|
||||||
|
ExifInfo does not provide access to the EXIF info in the actual image file
|
||||||
|
Some or all of the fields may be None
|
||||||
|
Only valid for Photos 5; on earlier database returns None
|
||||||
|
"""
|
||||||
|
|
||||||
|
if self._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
|
logging.debug(f"exif_info not implemented for this database version")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
exif = self._db._db_exifinfo_uuid[self.uuid]
|
||||||
|
exif_info = ExifInfo(
|
||||||
|
iso=exif["ZISO"],
|
||||||
|
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
||||||
|
metering_mode=exif["ZMETERINGMODE"],
|
||||||
|
sample_rate=exif["ZSAMPLERATE"],
|
||||||
|
track_format=exif["ZTRACKFORMAT"],
|
||||||
|
white_balance=exif["ZWHITEBALANCE"],
|
||||||
|
aperture=exif["ZAPERTURE"],
|
||||||
|
bit_rate=exif["ZBITRATE"],
|
||||||
|
duration=exif["ZDURATION"],
|
||||||
|
exposure_bias=exif["ZEXPOSUREBIAS"],
|
||||||
|
focal_length=exif["ZFOCALLENGTH"],
|
||||||
|
fps=exif["ZFPS"],
|
||||||
|
latitude=exif["ZLATITUDE"],
|
||||||
|
longitude=exif["ZLONGITUDE"],
|
||||||
|
shutter_speed=exif["ZSHUTTERSPEED"],
|
||||||
|
camera_make=exif["ZCAMERAMAKE"],
|
||||||
|
camera_model=exif["ZCAMERAMODEL"],
|
||||||
|
codec=exif["ZCODEC"],
|
||||||
|
lens_model=exif["ZLENSMODEL"],
|
||||||
|
)
|
||||||
|
except KeyError:
|
||||||
|
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
||||||
|
exif_info = ExifInfo(
|
||||||
|
iso=None,
|
||||||
|
flash_fired=None,
|
||||||
|
metering_mode=None,
|
||||||
|
sample_rate=None,
|
||||||
|
track_format=None,
|
||||||
|
white_balance=None,
|
||||||
|
aperture=None,
|
||||||
|
bit_rate=None,
|
||||||
|
duration=None,
|
||||||
|
exposure_bias=None,
|
||||||
|
focal_length=None,
|
||||||
|
fps=None,
|
||||||
|
latitude=None,
|
||||||
|
longitude=None,
|
||||||
|
shutter_speed=None,
|
||||||
|
camera_make=None,
|
||||||
|
camera_model=None,
|
||||||
|
codec=None,
|
||||||
|
lens_model=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return exif_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def exiftool(self):
|
||||||
|
"""Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
||||||
|
Requires that exiftool (https://exiftool.org/) be installed
|
||||||
|
If exiftool not installed, logs warning and returns None
|
||||||
|
If photo path is missing, returns None
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# return the memoized instance if it exists
|
||||||
|
return self._exiftool
|
||||||
|
except AttributeError:
|
||||||
|
try:
|
||||||
|
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
||||||
|
if self.path is not None and os.path.isfile(self.path):
|
||||||
|
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
||||||
|
else:
|
||||||
|
exiftool = None
|
||||||
|
except FileNotFoundError:
|
||||||
|
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
||||||
|
exiftool = None
|
||||||
|
logging.warning(
|
||||||
|
"exiftool not in path; download and install from https://exiftool.org/"
|
||||||
|
)
|
||||||
|
|
||||||
|
self._exiftool = exiftool
|
||||||
|
return self._exiftool
|
||||||
|
|
||||||
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
def detected_text(self, confidence_threshold=TEXT_DETECTION_CONFIDENCE_THRESHOLD):
|
||||||
"""Detects text in photo and returns lists of results as (detected text, confidence)
|
"""Detects text in photo and returns lists of results as (detected text, confidence)
|
||||||
@@ -1213,6 +1406,108 @@ class PhotoInfo:
|
|||||||
"""Returns latitude, in degrees"""
|
"""Returns latitude, in degrees"""
|
||||||
return self._info["latitude"]
|
return self._info["latitude"]
|
||||||
|
|
||||||
|
def render_template(
|
||||||
|
self, template_str: str, options: Optional[RenderOptions] = None
|
||||||
|
):
|
||||||
|
"""Renders a template string for PhotoInfo instance using PhotoTemplate
|
||||||
|
|
||||||
|
Args:
|
||||||
|
template_str: a template string with fields to render
|
||||||
|
options: a RenderOptions instance
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
([rendered_strings], [unmatched]): tuple of list of rendered strings and list of unmatched template values
|
||||||
|
"""
|
||||||
|
options = options or RenderOptions()
|
||||||
|
template = PhotoTemplate(self, exiftool_path=self._db._exiftool_path)
|
||||||
|
return template.render(template_str, options)
|
||||||
|
|
||||||
|
def export(
|
||||||
|
self,
|
||||||
|
dest,
|
||||||
|
filename=None,
|
||||||
|
edited=False,
|
||||||
|
live_photo=False,
|
||||||
|
raw_photo=False,
|
||||||
|
export_as_hardlink=False,
|
||||||
|
overwrite=False,
|
||||||
|
increment=True,
|
||||||
|
sidecar_json=False,
|
||||||
|
sidecar_exiftool=False,
|
||||||
|
sidecar_xmp=False,
|
||||||
|
use_photos_export=False,
|
||||||
|
timeout=120,
|
||||||
|
exiftool=False,
|
||||||
|
use_albums_as_keywords=False,
|
||||||
|
use_persons_as_keywords=False,
|
||||||
|
keyword_template=None,
|
||||||
|
description_template=None,
|
||||||
|
render_options: Optional[RenderOptions] = None,
|
||||||
|
):
|
||||||
|
"""export photo
|
||||||
|
dest: must be valid destination path (or exception raised)
|
||||||
|
filename: (optional): name of exported picture; if not provided, will use current filename
|
||||||
|
**NOTE**: if provided, user must ensure file extension (suffix) is correct.
|
||||||
|
For example, if photo is .CR2 file, edited image may be .jpeg.
|
||||||
|
If you provide an extension different than what the actual file is,
|
||||||
|
export will print a warning but will export the photo using the
|
||||||
|
incorrect file extension (unless use_photos_export is true, in which case export will
|
||||||
|
use the extension provided by Photos upon export; in this case, an incorrect extension is
|
||||||
|
silently ignored).
|
||||||
|
e.g. to get the extension of the edited photo,
|
||||||
|
reference PhotoInfo.path_edited
|
||||||
|
edited: (boolean, default=False); if True will export the edited version of the photo, otherwise exports the original version
|
||||||
|
(or raise exception if no edited version)
|
||||||
|
live_photo: (boolean, default=False); if True, will also export the associated .mov for live photos
|
||||||
|
raw_photo: (boolean, default=False); if True, will also export the associated RAW photo
|
||||||
|
export_as_hardlink: (boolean, default=False); if True, will hardlink files instead of copying them
|
||||||
|
overwrite: (boolean, default=False); if True will overwrite files if they already exist
|
||||||
|
increment: (boolean, default=True); if True, will increment file name until a non-existant name is found
|
||||||
|
if overwrite=False and increment=False, export will fail if destination file already exists
|
||||||
|
sidecar_json: if set will write a json sidecar with data in format readable by exiftool
|
||||||
|
sidecar filename will be dest/filename.json; includes exiftool tag group names (e.g. `exiftool -G -j`)
|
||||||
|
sidecar_exiftool: if set will write a json sidecar with data in format readable by exiftool
|
||||||
|
sidecar filename will be dest/filename.json; does not include exiftool tag group names (e.g. `exiftool -j`)
|
||||||
|
sidecar_xmp: if set will write an XMP sidecar with IPTC data
|
||||||
|
sidecar filename will be dest/filename.xmp
|
||||||
|
use_photos_export: (boolean, default=False); if True will attempt to export photo via applescript interaction with Photos
|
||||||
|
timeout: (int, default=120) timeout in seconds used with use_photos_export
|
||||||
|
exiftool: (boolean, default = False); if True, will use exiftool to write metadata to export file
|
||||||
|
returns list of full paths to the exported files
|
||||||
|
use_albums_as_keywords: (boolean, default = False); if True, will include album names in keywords
|
||||||
|
when exporting metadata with exiftool or sidecar
|
||||||
|
use_persons_as_keywords: (boolean, default = False); if True, will include person names in keywords
|
||||||
|
when exporting metadata with exiftool or sidecar
|
||||||
|
keyword_template: (list of strings); list of template strings that will be rendered as used as keywords
|
||||||
|
description_template: string; optional template string that will be rendered for use as photo description
|
||||||
|
render_options: an optional osxphotos.phototemplate.RenderOptions instance with options to pass to template renderer
|
||||||
|
|
||||||
|
Returns: list of photos exported
|
||||||
|
"""
|
||||||
|
|
||||||
|
exporter = PhotoExporter(self)
|
||||||
|
return exporter.export(
|
||||||
|
dest=dest,
|
||||||
|
filename=filename,
|
||||||
|
edited=edited,
|
||||||
|
live_photo=live_photo,
|
||||||
|
raw_photo=raw_photo,
|
||||||
|
export_as_hardlink=export_as_hardlink,
|
||||||
|
overwrite=overwrite,
|
||||||
|
increment=increment,
|
||||||
|
sidecar_json=sidecar_json,
|
||||||
|
sidecar_exiftool=sidecar_exiftool,
|
||||||
|
sidecar_xmp=sidecar_xmp,
|
||||||
|
use_photos_export=use_photos_export,
|
||||||
|
timeout=timeout,
|
||||||
|
exiftool=exiftool,
|
||||||
|
use_albums_as_keywords=use_albums_as_keywords,
|
||||||
|
use_persons_as_keywords=use_persons_as_keywords,
|
||||||
|
keyword_template=keyword_template,
|
||||||
|
description_template=description_template,
|
||||||
|
render_options=render_options,
|
||||||
|
)
|
||||||
|
|
||||||
def _get_album_uuids(self, project=False):
|
def _get_album_uuids(self, project=False):
|
||||||
"""Return list of album UUIDs this photo is found in
|
"""Return list of album UUIDs this photo is found in
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
"""
|
|
||||||
PhotoInfo class
|
|
||||||
Represents a single photo in the Photos library and provides access to the photo's attributes
|
|
||||||
PhotosDB.photos() returns a list of PhotoInfo objects
|
|
||||||
"""
|
|
||||||
|
|
||||||
from ._photoinfo_exifinfo import ExifInfo
|
|
||||||
from ._photoinfo_export import ExportResults
|
|
||||||
from ._photoinfo_scoreinfo import ScoreInfo
|
|
||||||
from .photoinfo import PhotoInfo, PhotoInfoNone
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose comments and likes for shared photos """
|
|
||||||
|
|
||||||
@property
|
|
||||||
def comments(self):
|
|
||||||
""" Returns list of Comment objects for any comments on the photo (sorted by date) """
|
|
||||||
try:
|
|
||||||
return self._db._db_comments_uuid[self.uuid]["comments"]
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@property
|
|
||||||
def likes(self):
|
|
||||||
""" Returns list of Like objects for any likes on the photo (sorted by date) """
|
|
||||||
try:
|
|
||||||
return self._db._db_comments_uuid[self.uuid]["likes"]
|
|
||||||
except:
|
|
||||||
return []
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose EXIF info from the library """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from .._constants import _PHOTOS_4_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ExifInfo:
|
|
||||||
""" EXIF info associated with a photo from the Photos library """
|
|
||||||
|
|
||||||
flash_fired: bool
|
|
||||||
iso: int
|
|
||||||
metering_mode: int
|
|
||||||
sample_rate: int
|
|
||||||
track_format: int
|
|
||||||
white_balance: int
|
|
||||||
aperture: float
|
|
||||||
bit_rate: float
|
|
||||||
duration: float
|
|
||||||
exposure_bias: float
|
|
||||||
focal_length: float
|
|
||||||
fps: float
|
|
||||||
latitude: float
|
|
||||||
longitude: float
|
|
||||||
shutter_speed: float
|
|
||||||
camera_make: str
|
|
||||||
camera_model: str
|
|
||||||
codec: str
|
|
||||||
lens_model: str
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exif_info(self):
|
|
||||||
""" Returns an ExifInfo object with the EXIF data for photo
|
|
||||||
Note: the returned EXIF data is the data Photos stores in the database on import;
|
|
||||||
ExifInfo does not provide access to the EXIF info in the actual image file
|
|
||||||
Some or all of the fields may be None
|
|
||||||
Only valid for Photos 5; on earlier database returns None
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
logging.debug(f"exif_info not implemented for this database version")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
exif = self._db._db_exifinfo_uuid[self.uuid]
|
|
||||||
exif_info = ExifInfo(
|
|
||||||
iso=exif["ZISO"],
|
|
||||||
flash_fired=True if exif["ZFLASHFIRED"] == 1 else False,
|
|
||||||
metering_mode=exif["ZMETERINGMODE"],
|
|
||||||
sample_rate=exif["ZSAMPLERATE"],
|
|
||||||
track_format=exif["ZTRACKFORMAT"],
|
|
||||||
white_balance=exif["ZWHITEBALANCE"],
|
|
||||||
aperture=exif["ZAPERTURE"],
|
|
||||||
bit_rate=exif["ZBITRATE"],
|
|
||||||
duration=exif["ZDURATION"],
|
|
||||||
exposure_bias=exif["ZEXPOSUREBIAS"],
|
|
||||||
focal_length=exif["ZFOCALLENGTH"],
|
|
||||||
fps=exif["ZFPS"],
|
|
||||||
latitude=exif["ZLATITUDE"],
|
|
||||||
longitude=exif["ZLONGITUDE"],
|
|
||||||
shutter_speed=exif["ZSHUTTERSPEED"],
|
|
||||||
camera_make=exif["ZCAMERAMAKE"],
|
|
||||||
camera_model=exif["ZCAMERAMODEL"],
|
|
||||||
codec=exif["ZCODEC"],
|
|
||||||
lens_model=exif["ZLENSMODEL"],
|
|
||||||
)
|
|
||||||
except KeyError:
|
|
||||||
logging.debug(f"Could not find exif record for uuid {self.uuid}")
|
|
||||||
exif_info = ExifInfo(
|
|
||||||
iso=None,
|
|
||||||
flash_fired=None,
|
|
||||||
metering_mode=None,
|
|
||||||
sample_rate=None,
|
|
||||||
track_format=None,
|
|
||||||
white_balance=None,
|
|
||||||
aperture=None,
|
|
||||||
bit_rate=None,
|
|
||||||
duration=None,
|
|
||||||
exposure_bias=None,
|
|
||||||
focal_length=None,
|
|
||||||
fps=None,
|
|
||||||
latitude=None,
|
|
||||||
longitude=None,
|
|
||||||
shutter_speed=None,
|
|
||||||
camera_make=None,
|
|
||||||
camera_model=None,
|
|
||||||
codec=None,
|
|
||||||
lens_model=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
return exif_info
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
""" Implementation for PhotoInfo.exiftool property which returns ExifTool object for a photo """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
|
|
||||||
from ..exiftool import ExifToolCaching, get_exiftool_path
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def exiftool(self):
|
|
||||||
""" Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo.
|
|
||||||
Requires that exiftool (https://exiftool.org/) be installed
|
|
||||||
If exiftool not installed, logs warning and returns None
|
|
||||||
If photo path is missing, returns None
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# return the memoized instance if it exists
|
|
||||||
return self._exiftool
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
exiftool_path = self._db._exiftool_path or get_exiftool_path()
|
|
||||||
if self.path is not None and os.path.isfile(self.path):
|
|
||||||
exiftool = ExifToolCaching(self.path, exiftool=exiftool_path)
|
|
||||||
else:
|
|
||||||
exiftool = None
|
|
||||||
except FileNotFoundError:
|
|
||||||
# get_exiftool_path raises FileNotFoundError if exiftool not found
|
|
||||||
exiftool = None
|
|
||||||
logging.warning(
|
|
||||||
f"exiftool not in path; download and install from https://exiftool.org/"
|
|
||||||
)
|
|
||||||
|
|
||||||
self._exiftool = exiftool
|
|
||||||
return self._exiftool
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,119 +0,0 @@
|
|||||||
""" PhotoInfo methods to expose computed score info from the library """
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from .._constants import _PHOTOS_4_VERSION
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class ScoreInfo:
|
|
||||||
""" Computed photo score info associated with a photo from the Photos library """
|
|
||||||
|
|
||||||
overall: float
|
|
||||||
curation: float
|
|
||||||
promotion: float
|
|
||||||
highlight_visibility: float
|
|
||||||
behavioral: float
|
|
||||||
failure: float
|
|
||||||
harmonious_color: float
|
|
||||||
immersiveness: float
|
|
||||||
interaction: float
|
|
||||||
interesting_subject: float
|
|
||||||
intrusive_object_presence: float
|
|
||||||
lively_color: float
|
|
||||||
low_light: float
|
|
||||||
noise: float
|
|
||||||
pleasant_camera_tilt: float
|
|
||||||
pleasant_composition: float
|
|
||||||
pleasant_lighting: float
|
|
||||||
pleasant_pattern: float
|
|
||||||
pleasant_perspective: float
|
|
||||||
pleasant_post_processing: float
|
|
||||||
pleasant_reflection: float
|
|
||||||
pleasant_symmetry: float
|
|
||||||
sharply_focused_subject: float
|
|
||||||
tastefully_blurred: float
|
|
||||||
well_chosen_subject: float
|
|
||||||
well_framed_subject: float
|
|
||||||
well_timed_shot: float
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def score(self):
|
|
||||||
""" Computed score information for a photo
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ScoreInfo instance
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
logging.debug(f"score not implemented for this database version")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
return self._scoreinfo # pylint: disable=access-member-before-definition
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
scores = self._db._db_scoreinfo_uuid[self.uuid]
|
|
||||||
self._scoreinfo = ScoreInfo(
|
|
||||||
overall=scores["overall_aesthetic"],
|
|
||||||
curation=scores["curation"],
|
|
||||||
promotion=scores["promotion"],
|
|
||||||
highlight_visibility=scores["highlight_visibility"],
|
|
||||||
behavioral=scores["behavioral"],
|
|
||||||
failure=scores["failure"],
|
|
||||||
harmonious_color=scores["harmonious_color"],
|
|
||||||
immersiveness=scores["immersiveness"],
|
|
||||||
interaction=scores["interaction"],
|
|
||||||
interesting_subject=scores["interesting_subject"],
|
|
||||||
intrusive_object_presence=scores["intrusive_object_presence"],
|
|
||||||
lively_color=scores["lively_color"],
|
|
||||||
low_light=scores["low_light"],
|
|
||||||
noise=scores["noise"],
|
|
||||||
pleasant_camera_tilt=scores["pleasant_camera_tilt"],
|
|
||||||
pleasant_composition=scores["pleasant_composition"],
|
|
||||||
pleasant_lighting=scores["pleasant_lighting"],
|
|
||||||
pleasant_pattern=scores["pleasant_pattern"],
|
|
||||||
pleasant_perspective=scores["pleasant_perspective"],
|
|
||||||
pleasant_post_processing=scores["pleasant_post_processing"],
|
|
||||||
pleasant_reflection=scores["pleasant_reflection"],
|
|
||||||
pleasant_symmetry=scores["pleasant_symmetry"],
|
|
||||||
sharply_focused_subject=scores["sharply_focused_subject"],
|
|
||||||
tastefully_blurred=scores["tastefully_blurred"],
|
|
||||||
well_chosen_subject=scores["well_chosen_subject"],
|
|
||||||
well_framed_subject=scores["well_framed_subject"],
|
|
||||||
well_timed_shot=scores["well_timed_shot"],
|
|
||||||
)
|
|
||||||
return self._scoreinfo
|
|
||||||
except KeyError:
|
|
||||||
self._scoreinfo = ScoreInfo(
|
|
||||||
overall=0.0,
|
|
||||||
curation=0.0,
|
|
||||||
promotion=0.0,
|
|
||||||
highlight_visibility=0.0,
|
|
||||||
behavioral=0.0,
|
|
||||||
failure=0.0,
|
|
||||||
harmonious_color=0.0,
|
|
||||||
immersiveness=0.0,
|
|
||||||
interaction=0.0,
|
|
||||||
interesting_subject=0.0,
|
|
||||||
intrusive_object_presence=0.0,
|
|
||||||
lively_color=0.0,
|
|
||||||
low_light=0.0,
|
|
||||||
noise=0.0,
|
|
||||||
pleasant_camera_tilt=0.0,
|
|
||||||
pleasant_composition=0.0,
|
|
||||||
pleasant_lighting=0.0,
|
|
||||||
pleasant_pattern=0.0,
|
|
||||||
pleasant_perspective=0.0,
|
|
||||||
pleasant_post_processing=0.0,
|
|
||||||
pleasant_reflection=0.0,
|
|
||||||
pleasant_symmetry=0.0,
|
|
||||||
sharply_focused_subject=0.0,
|
|
||||||
tastefully_blurred=0.0,
|
|
||||||
well_chosen_subject=0.0,
|
|
||||||
well_framed_subject=0.0,
|
|
||||||
well_timed_shot=0.0,
|
|
||||||
)
|
|
||||||
return self._scoreinfo
|
|
||||||
39
osxphotos/scoreinfo.py
Normal file
39
osxphotos/scoreinfo.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
""" ScoreInfo class to expose computed score info from the library """
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from ._constants import _PHOTOS_4_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class ScoreInfo:
|
||||||
|
""" Computed photo score info associated with a photo from the Photos library """
|
||||||
|
|
||||||
|
overall: float
|
||||||
|
curation: float
|
||||||
|
promotion: float
|
||||||
|
highlight_visibility: float
|
||||||
|
behavioral: float
|
||||||
|
failure: float
|
||||||
|
harmonious_color: float
|
||||||
|
immersiveness: float
|
||||||
|
interaction: float
|
||||||
|
interesting_subject: float
|
||||||
|
intrusive_object_presence: float
|
||||||
|
lively_color: float
|
||||||
|
low_light: float
|
||||||
|
noise: float
|
||||||
|
pleasant_camera_tilt: float
|
||||||
|
pleasant_composition: float
|
||||||
|
pleasant_lighting: float
|
||||||
|
pleasant_pattern: float
|
||||||
|
pleasant_perspective: float
|
||||||
|
pleasant_post_processing: float
|
||||||
|
pleasant_reflection: float
|
||||||
|
pleasant_symmetry: float
|
||||||
|
sharply_focused_subject: float
|
||||||
|
tastefully_blurred: float
|
||||||
|
well_chosen_subject: float
|
||||||
|
well_framed_subject: float
|
||||||
|
well_timed_shot: float
|
||||||
|
|
||||||
@@ -1,98 +1,39 @@
|
|||||||
""" Methods and class for PhotoInfo exposing SearchInfo data such as labels
|
""" class for PhotoInfo exposing SearchInfo data such as labels
|
||||||
Adds the following properties to PhotoInfo (valid only for Photos 5):
|
|
||||||
search_info: returns a SearchInfo object
|
|
||||||
search_info_normalized: returns a SearchInfo object with properties that produce normalized results
|
|
||||||
labels: returns list of labels
|
|
||||||
labels_normalized: returns list of normalized labels
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .._constants import (
|
from ._constants import (
|
||||||
_PHOTOS_4_VERSION,
|
_PHOTOS_4_VERSION,
|
||||||
|
SEARCH_CATEGORY_ACTIVITY,
|
||||||
|
SEARCH_CATEGORY_ALL_LOCALITY,
|
||||||
|
SEARCH_CATEGORY_BODY_OF_WATER,
|
||||||
SEARCH_CATEGORY_CITY,
|
SEARCH_CATEGORY_CITY,
|
||||||
|
SEARCH_CATEGORY_COUNTRY,
|
||||||
|
SEARCH_CATEGORY_HOLIDAY,
|
||||||
SEARCH_CATEGORY_LABEL,
|
SEARCH_CATEGORY_LABEL,
|
||||||
|
SEARCH_CATEGORY_MEDIA_TYPES,
|
||||||
|
SEARCH_CATEGORY_MONTH,
|
||||||
SEARCH_CATEGORY_NEIGHBORHOOD,
|
SEARCH_CATEGORY_NEIGHBORHOOD,
|
||||||
SEARCH_CATEGORY_PLACE_NAME,
|
SEARCH_CATEGORY_PLACE_NAME,
|
||||||
SEARCH_CATEGORY_STREET,
|
SEARCH_CATEGORY_SEASON,
|
||||||
SEARCH_CATEGORY_ALL_LOCALITY,
|
|
||||||
SEARCH_CATEGORY_COUNTRY,
|
|
||||||
SEARCH_CATEGORY_STATE,
|
SEARCH_CATEGORY_STATE,
|
||||||
SEARCH_CATEGORY_STATE_ABBREVIATION,
|
SEARCH_CATEGORY_STATE_ABBREVIATION,
|
||||||
SEARCH_CATEGORY_BODY_OF_WATER,
|
SEARCH_CATEGORY_STREET,
|
||||||
SEARCH_CATEGORY_MONTH,
|
|
||||||
SEARCH_CATEGORY_YEAR,
|
|
||||||
SEARCH_CATEGORY_HOLIDAY,
|
|
||||||
SEARCH_CATEGORY_ACTIVITY,
|
|
||||||
SEARCH_CATEGORY_SEASON,
|
|
||||||
SEARCH_CATEGORY_VENUE,
|
SEARCH_CATEGORY_VENUE,
|
||||||
SEARCH_CATEGORY_VENUE_TYPE,
|
SEARCH_CATEGORY_VENUE_TYPE,
|
||||||
SEARCH_CATEGORY_MEDIA_TYPES,
|
SEARCH_CATEGORY_YEAR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_info(self):
|
|
||||||
""" returns SearchInfo object for photo
|
|
||||||
only valid on Photos 5, on older libraries, returns None
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# memoize SearchInfo object
|
|
||||||
try:
|
|
||||||
return self._search_info
|
|
||||||
except AttributeError:
|
|
||||||
self._search_info = SearchInfo(self)
|
|
||||||
return self._search_info
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def search_info_normalized(self):
|
|
||||||
""" returns SearchInfo object for photo that produces normalized results
|
|
||||||
only valid on Photos 5, on older libraries, returns None
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return None
|
|
||||||
|
|
||||||
# memoize SearchInfo object
|
|
||||||
try:
|
|
||||||
return self._search_info_normalized
|
|
||||||
except AttributeError:
|
|
||||||
self._search_info_normalized = SearchInfo(self, normalized=True)
|
|
||||||
return self._search_info_normalized
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def labels(self):
|
|
||||||
""" returns list of labels applied to photo by Photos image categorization
|
|
||||||
only valid on Photos 5, on older libraries returns empty list
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return self.search_info.labels
|
|
||||||
|
|
||||||
|
|
||||||
@property
|
|
||||||
def labels_normalized(self):
|
|
||||||
""" returns normalized list of labels applied to photo by Photos image categorization
|
|
||||||
only valid on Photos 5, on older libraries returns empty list
|
|
||||||
"""
|
|
||||||
if self._db._db_version <= _PHOTOS_4_VERSION:
|
|
||||||
return []
|
|
||||||
|
|
||||||
return self.search_info_normalized.labels
|
|
||||||
|
|
||||||
|
|
||||||
class SearchInfo:
|
class SearchInfo:
|
||||||
""" Info about search terms such as machine learning labels that Photos knows about a photo """
|
"""Info about search terms such as machine learning labels that Photos knows about a photo"""
|
||||||
|
|
||||||
def __init__(self, photo, normalized=False):
|
def __init__(self, photo, normalized=False):
|
||||||
""" photo: PhotoInfo object
|
"""photo: PhotoInfo object
|
||||||
normalized: if True, all properties return normalized (lower case) results """
|
normalized: if True, all properties return normalized (lower case) results"""
|
||||||
|
|
||||||
if photo._db._db_version <= _PHOTOS_4_VERSION:
|
if photo._db._db_version <= _PHOTOS_4_VERSION:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f"search info not implemented for this database version"
|
"search info not implemented for this database version"
|
||||||
)
|
)
|
||||||
|
|
||||||
self._photo = photo
|
self._photo = photo
|
||||||
@@ -107,27 +48,27 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def labels(self):
|
def labels(self):
|
||||||
""" return list of labels associated with Photo """
|
"""return list of labels associated with Photo"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
|
return self._get_text_for_category(SEARCH_CATEGORY_LABEL)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def place_names(self):
|
def place_names(self):
|
||||||
""" returns list of place names """
|
"""returns list of place names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
|
return self._get_text_for_category(SEARCH_CATEGORY_PLACE_NAME)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def streets(self):
|
def streets(self):
|
||||||
""" returns list of street names """
|
"""returns list of street names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
|
return self._get_text_for_category(SEARCH_CATEGORY_STREET)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def neighborhoods(self):
|
def neighborhoods(self):
|
||||||
""" returns list of neighborhoods """
|
"""returns list of neighborhoods"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
|
return self._get_text_for_category(SEARCH_CATEGORY_NEIGHBORHOOD)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def locality_names(self):
|
def locality_names(self):
|
||||||
""" returns list of other locality names """
|
"""returns list of other locality names"""
|
||||||
locality = []
|
locality = []
|
||||||
for category in SEARCH_CATEGORY_ALL_LOCALITY:
|
for category in SEARCH_CATEGORY_ALL_LOCALITY:
|
||||||
locality += self._get_text_for_category(category)
|
locality += self._get_text_for_category(category)
|
||||||
@@ -135,74 +76,74 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def city(self):
|
def city(self):
|
||||||
""" returns city/town """
|
"""returns city/town"""
|
||||||
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
|
city = self._get_text_for_category(SEARCH_CATEGORY_CITY)
|
||||||
return city[0] if city else ""
|
return city[0] if city else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
""" returns state name """
|
"""returns state name"""
|
||||||
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
|
state = self._get_text_for_category(SEARCH_CATEGORY_STATE)
|
||||||
return state[0] if state else ""
|
return state[0] if state else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state_abbreviation(self):
|
def state_abbreviation(self):
|
||||||
""" returns state abbreviation """
|
"""returns state abbreviation"""
|
||||||
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
|
abbrev = self._get_text_for_category(SEARCH_CATEGORY_STATE_ABBREVIATION)
|
||||||
return abbrev[0] if abbrev else ""
|
return abbrev[0] if abbrev else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def country(self):
|
def country(self):
|
||||||
""" returns country name """
|
"""returns country name"""
|
||||||
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
|
country = self._get_text_for_category(SEARCH_CATEGORY_COUNTRY)
|
||||||
return country[0] if country else ""
|
return country[0] if country else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def month(self):
|
def month(self):
|
||||||
""" returns month name """
|
"""returns month name"""
|
||||||
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
|
month = self._get_text_for_category(SEARCH_CATEGORY_MONTH)
|
||||||
return month[0] if month else ""
|
return month[0] if month else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def year(self):
|
def year(self):
|
||||||
""" returns year """
|
"""returns year"""
|
||||||
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
|
year = self._get_text_for_category(SEARCH_CATEGORY_YEAR)
|
||||||
return year[0] if year else ""
|
return year[0] if year else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bodies_of_water(self):
|
def bodies_of_water(self):
|
||||||
""" returns list of body of water names """
|
"""returns list of body of water names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
|
return self._get_text_for_category(SEARCH_CATEGORY_BODY_OF_WATER)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def holidays(self):
|
def holidays(self):
|
||||||
""" returns list of holiday names """
|
"""returns list of holiday names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
|
return self._get_text_for_category(SEARCH_CATEGORY_HOLIDAY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def activities(self):
|
def activities(self):
|
||||||
""" returns list of activity names """
|
"""returns list of activity names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
|
return self._get_text_for_category(SEARCH_CATEGORY_ACTIVITY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def season(self):
|
def season(self):
|
||||||
""" returns season name """
|
"""returns season name"""
|
||||||
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
|
season = self._get_text_for_category(SEARCH_CATEGORY_SEASON)
|
||||||
return season[0] if season else ""
|
return season[0] if season else ""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def venues(self):
|
def venues(self):
|
||||||
""" returns list of venue names """
|
"""returns list of venue names"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
|
return self._get_text_for_category(SEARCH_CATEGORY_VENUE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def venue_types(self):
|
def venue_types(self):
|
||||||
""" returns list of venue types """
|
"""returns list of venue types"""
|
||||||
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
|
return self._get_text_for_category(SEARCH_CATEGORY_VENUE_TYPE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media_types(self):
|
def media_types(self):
|
||||||
""" returns list of media types (photo, video, panorama, etc) """
|
"""returns list of media types (photo, video, panorama, etc)"""
|
||||||
types = []
|
types = []
|
||||||
for category in SEARCH_CATEGORY_MEDIA_TYPES:
|
for category in SEARCH_CATEGORY_MEDIA_TYPES:
|
||||||
types += self._get_text_for_category(category)
|
types += self._get_text_for_category(category)
|
||||||
@@ -210,7 +151,7 @@ class SearchInfo:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self):
|
def all(self):
|
||||||
""" return all search info properties in a single list """
|
"""return all search info properties in a single list"""
|
||||||
all = (
|
all = (
|
||||||
self.labels
|
self.labels
|
||||||
+ self.place_names
|
+ self.place_names
|
||||||
@@ -242,7 +183,7 @@ class SearchInfo:
|
|||||||
return all
|
return all
|
||||||
|
|
||||||
def asdict(self):
|
def asdict(self):
|
||||||
""" return dict of search info """
|
"""return dict of search info"""
|
||||||
return {
|
return {
|
||||||
"labels": self.labels,
|
"labels": self.labels,
|
||||||
"place_names": self.place_names,
|
"place_names": self.place_names,
|
||||||
@@ -265,7 +206,7 @@ class SearchInfo:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def _get_text_for_category(self, category):
|
def _get_text_for_category(self, category):
|
||||||
""" return list of text for a specified category ID """
|
"""return list of text for a specified category ID"""
|
||||||
if self._db_searchinfo:
|
if self._db_searchinfo:
|
||||||
content = "normalized_string" if self._normalized else "content_string"
|
content = "normalized_string" if self._normalized else "content_string"
|
||||||
return [
|
return [
|
||||||
@@ -13,6 +13,7 @@ import pytest
|
|||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
from osxphotos.utils import _get_os_version
|
from osxphotos.utils import _get_os_version
|
||||||
|
|
||||||
OS_VERSION = _get_os_version()
|
OS_VERSION = _get_os_version()
|
||||||
@@ -1142,6 +1143,7 @@ def test_date_invalid():
|
|||||||
"""Test date is invalid"""
|
"""Test date is invalid"""
|
||||||
# doesn't run correctly with the module-level fixture
|
# doesn't run correctly with the module-level fixture
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
@@ -1396,7 +1398,7 @@ def test_exiftool_newlines_in_description(photosdb):
|
|||||||
"""Test that exiftool handles newlines embedded in description, issue #393"""
|
"""Test that exiftool handles newlines embedded in description, issue #393"""
|
||||||
|
|
||||||
photo = photosdb.get_photo(UUID_DICT["description_newlines"])
|
photo = photosdb.get_photo(UUID_DICT["description_newlines"])
|
||||||
exif = photo._exiftool_dict()
|
exif = PhotoExporter(photo)._exiftool_dict()
|
||||||
assert photo.description.find("\n") > 0
|
assert photo.description.find("\n") > 0
|
||||||
assert exif["EXIF:ImageDescription"].find("\n") > 0
|
assert exif["EXIF:ImageDescription"].find("\n") > 0
|
||||||
|
|
||||||
|
|||||||
@@ -4291,7 +4291,7 @@ def test_export_error(monkeypatch):
|
|||||||
def throw_error(*args, **kwargs):
|
def throw_error(*args, **kwargs):
|
||||||
raise ValueError("Argh!")
|
raise ValueError("Argh!")
|
||||||
|
|
||||||
monkeypatch.setattr(osxphotos.PhotoInfo, "export2", throw_error)
|
monkeypatch.setattr(osxphotos.PhotoExporter, "export2", throw_error)
|
||||||
with runner.isolated_filesystem():
|
with runner.isolated_filesystem():
|
||||||
result = runner.invoke(
|
result = runner.invoke(
|
||||||
export,
|
export,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from osxphotos.photoinfo import ExifInfo
|
from osxphotos.exifinfo import ExifInfo
|
||||||
|
|
||||||
PHOTOS_DB_5 = "tests/Test-Cloud-10.15.1.photoslibrary"
|
PHOTOS_DB_5 = "tests/Test-Cloud-10.15.1.photoslibrary"
|
||||||
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
||||||
@@ -82,14 +82,14 @@ def photosdb():
|
|||||||
|
|
||||||
|
|
||||||
def test_exif_info_v5(photosdb):
|
def test_exif_info_v5(photosdb):
|
||||||
""" test exif_info """
|
"""test exif_info"""
|
||||||
for uuid in EXIF_DICT:
|
for uuid in EXIF_DICT:
|
||||||
photo = photosdb.photos(uuid=[uuid], movies=True)[0]
|
photo = photosdb.photos(uuid=[uuid], movies=True)[0]
|
||||||
assert photo.exif_info == EXIF_DICT[uuid]
|
assert photo.exif_info == EXIF_DICT[uuid]
|
||||||
|
|
||||||
|
|
||||||
def test_exif_info_v4():
|
def test_exif_info_v4():
|
||||||
""" test version 4, exif_info should be None """
|
"""test version 4, exif_info should be None"""
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB_4)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import pytest
|
|||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
from osxphotos.exiftool import get_exiftool_path
|
from osxphotos.exiftool import get_exiftool_path
|
||||||
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
from osxphotos.utils import dd_to_dms_str
|
from osxphotos.utils import dd_to_dms_str
|
||||||
|
|
||||||
# determine if exiftool installed so exiftool tests can be skipped
|
# determine if exiftool installed so exiftool tests can be skipped
|
||||||
@@ -401,7 +402,7 @@ def test_exiftool_json_sidecar(photosdb):
|
|||||||
with open(str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.json"), "r") as fp:
|
with open(str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.json"), "r") as fp:
|
||||||
json_expected = json.load(fp)[0]
|
json_expected = json.load(fp)[0]
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar()
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar()
|
||||||
json_got = json.loads(json_got)[0]
|
json_got = json.loads(json_got)[0]
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -417,7 +418,7 @@ def test_exiftool_json_sidecar_ignore_date_modified(photosdb):
|
|||||||
) as fp:
|
) as fp:
|
||||||
json_expected = json.load(fp)[0]
|
json_expected = json.load(fp)[0]
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar(ignore_date_modified=True)
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar(ignore_date_modified=True)
|
||||||
json_got = json.loads(json_got)[0]
|
json_got = json.loads(json_got)[0]
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -448,7 +449,7 @@ def test_exiftool_json_sidecar_keyword_template_long(capsys, photosdb):
|
|||||||
|
|
||||||
long_str = "x" * (_MAX_IPTC_KEYWORD_LEN + 1)
|
long_str = "x" * (_MAX_IPTC_KEYWORD_LEN + 1)
|
||||||
photos[0]._verbose = print
|
photos[0]._verbose = print
|
||||||
json_got = photos[0]._exiftool_json_sidecar(keyword_template=[long_str])
|
json_got = PhotoExporter(photos[0])._exiftool_json_sidecar(keyword_template=[long_str])
|
||||||
json_got = json.loads(json_got)[0]
|
json_got = json.loads(json_got)[0]
|
||||||
|
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
@@ -483,7 +484,7 @@ def test_exiftool_json_sidecar_keyword_template(photosdb):
|
|||||||
str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.json"), "r"
|
str(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.json"), "r"
|
||||||
) as fp:
|
) as fp:
|
||||||
json_expected = json.load(fp)
|
json_expected = json.load(fp)
|
||||||
json_got = photo._exiftool_json_sidecar(keyword_template=["{folder_album}"])
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar(keyword_template=["{folder_album}"])
|
||||||
json_got = json.loads(json_got)
|
json_got = json.loads(json_got)
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -499,7 +500,7 @@ def test_exiftool_json_sidecar_use_persons_keyword(photosdb):
|
|||||||
) as fp:
|
) as fp:
|
||||||
json_expected = json.load(fp)[0]
|
json_expected = json.load(fp)[0]
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar(use_persons_as_keywords=True)
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar(use_persons_as_keywords=True)
|
||||||
json_got = json.loads(json_got)[0]
|
json_got = json.loads(json_got)[0]
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -515,7 +516,7 @@ def test_exiftool_json_sidecar_use_albums_keywords(photosdb):
|
|||||||
) as fp:
|
) as fp:
|
||||||
json_expected = json.load(fp)
|
json_expected = json.load(fp)
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar(use_albums_as_keywords=True)
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar(use_albums_as_keywords=True)
|
||||||
json_got = json.loads(json_got)
|
json_got = json.loads(json_got)
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -528,7 +529,7 @@ def test_exiftool_sidecar(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_no_tag_groups.json", "r") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_no_tag_groups.json", "r") as fp:
|
||||||
json_expected = fp.read()
|
json_expected = fp.read()
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar(tag_groups=False)
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar(tag_groups=False)
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
|
|
||||||
@@ -554,7 +555,7 @@ def test_xmp_sidecar(photosdb):
|
|||||||
|
|
||||||
with open(f"tests/sidecars/{uuid}.xmp", "r") as file:
|
with open(f"tests/sidecars/{uuid}.xmp", "r") as file:
|
||||||
xmp_expected = file.read()
|
xmp_expected = file.read()
|
||||||
xmp_got = photos[0]._xmp_sidecar(extension="jpg")
|
xmp_got = PhotoExporter(photos[0])._xmp_sidecar(extension="jpg")
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
|
|
||||||
@@ -568,7 +569,7 @@ def test_xmp_sidecar_extension(photosdb):
|
|||||||
xmp_expected = file.read()
|
xmp_expected = file.read()
|
||||||
xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
|
xmp_expected_lines = [line.strip() for line in xmp_expected.split("\n")]
|
||||||
|
|
||||||
xmp_got = photos[0]._xmp_sidecar()
|
xmp_got = PhotoExporter(photos[0])._xmp_sidecar()
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
|
|
||||||
@@ -580,7 +581,7 @@ def test_xmp_sidecar_use_persons_keyword(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_persons_as_keywords.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_persons_as_keywords.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar(use_persons_as_keywords=True, extension="jpg")
|
xmp_got = PhotoExporter(photo)._xmp_sidecar(use_persons_as_keywords=True, extension="jpg")
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
|
|
||||||
@@ -592,7 +593,7 @@ def test_xmp_sidecar_use_albums_keyword(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_albums_as_keywords.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_albums_as_keywords.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar(use_albums_as_keywords=True, extension="jpg")
|
xmp_got = PhotoExporter(photo)._xmp_sidecar(use_albums_as_keywords=True, extension="jpg")
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
|
|
||||||
@@ -605,7 +606,7 @@ def test_xmp_sidecar_gps(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar()
|
xmp_got = PhotoExporter(photo)._xmp_sidecar()
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
|
|
||||||
@@ -617,7 +618,7 @@ def test_xmp_sidecar_keyword_template(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar(
|
xmp_got = PhotoExporter(photo)._xmp_sidecar(
|
||||||
keyword_template=["{created.year}", "{folder_album}"], extension="jpg"
|
keyword_template=["{created.year}", "{folder_album}"], extension="jpg"
|
||||||
)
|
)
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
|
|
||||||
skip_test = "OSXPHOTOS_TEST_CONVERT" not in os.environ
|
skip_test = "OSXPHOTOS_TEST_CONVERT" not in os.environ
|
||||||
pytestmark = pytest.mark.skipif(
|
pytestmark = pytest.mark.skipif(
|
||||||
@@ -43,7 +44,7 @@ def test_export_convert_raw_to_jpeg(photosdb):
|
|||||||
dest = tempdir.name
|
dest = tempdir.name
|
||||||
photos = photosdb.photos(uuid=[UUID_DICT["raw"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["raw"]])
|
||||||
|
|
||||||
results = photos[0].export2(dest, convert_to_jpeg=True)
|
results = PhotoExporter(photos[0]).export2(dest, convert_to_jpeg=True)
|
||||||
got_dest = pathlib.Path(results.exported[0])
|
got_dest = pathlib.Path(results.exported[0])
|
||||||
|
|
||||||
assert got_dest.is_file()
|
assert got_dest.is_file()
|
||||||
@@ -60,7 +61,7 @@ def test_export_convert_heic_to_jpeg(photosdb):
|
|||||||
dest = tempdir.name
|
dest = tempdir.name
|
||||||
photos = photosdb.photos(uuid=[UUID_DICT["heic"]])
|
photos = photosdb.photos(uuid=[UUID_DICT["heic"]])
|
||||||
|
|
||||||
results = photos[0].export2(dest, convert_to_jpeg=True)
|
results = PhotoExporter(photos[0]).export2(dest, convert_to_jpeg=True)
|
||||||
got_dest = pathlib.Path(results.exported[0])
|
got_dest = pathlib.Path(results.exported[0])
|
||||||
|
|
||||||
assert got_dest.is_file()
|
assert got_dest.is_file()
|
||||||
@@ -87,7 +88,7 @@ def test_export_convert_live_heic_to_jpeg():
|
|||||||
dest = tempdir.name
|
dest = tempdir.name
|
||||||
photo = photosdb.get_photo(UUID_LIVE_HEIC)
|
photo = photosdb.get_photo(UUID_LIVE_HEIC)
|
||||||
|
|
||||||
results = photo.export2(dest, convert_to_jpeg=True, live_photo=True)
|
results = PhotoExporter(photo).export2(dest, convert_to_jpeg=True, live_photo=True)
|
||||||
|
|
||||||
for name in NAMES_LIVE_HEIC:
|
for name in NAMES_LIVE_HEIC:
|
||||||
assert f"{tempdir.name}/{name}" in results.exported
|
assert f"{tempdir.name}/{name}" in results.exported
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import json
|
||||||
|
import pathlib
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import json
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
import pathlib
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
|
|
||||||
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
PHOTOS_DB = "./tests/Test-10.14.6.photoslibrary/database/photos.db"
|
||||||
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
|
PHOTOS_DB_PATH = "/Test-10.14.6.photoslibrary/database/photos.db"
|
||||||
@@ -335,7 +337,7 @@ def test_exiftool_json_sidecar(photosdb):
|
|||||||
with open(str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.json"), "r") as fp:
|
with open(str(pathlib.Path(SIDECAR_DIR) / f"{uuid}.json"), "r") as fp:
|
||||||
json_expected = json.load(fp)[0]
|
json_expected = json.load(fp)[0]
|
||||||
|
|
||||||
json_got = photo._exiftool_json_sidecar()
|
json_got = PhotoExporter(photo)._exiftool_json_sidecar()
|
||||||
json_got = json.loads(json_got)[0]
|
json_got = json.loads(json_got)[0]
|
||||||
|
|
||||||
assert json_got == json_expected
|
assert json_got == json_expected
|
||||||
@@ -349,7 +351,7 @@ def test_xmp_sidecar(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_ext.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_ext.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar(extension="jpg")
|
xmp_got = PhotoExporter(photo)._xmp_sidecar(extension="jpg")
|
||||||
|
|
||||||
assert xmp_got == xmp_expected
|
assert xmp_got == xmp_expected
|
||||||
|
|
||||||
@@ -362,7 +364,7 @@ def test_xmp_sidecar_keyword_template(photosdb):
|
|||||||
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.xmp") as fp:
|
with open(pathlib.Path(SIDECAR_DIR) / f"{uuid}_keyword_template.xmp") as fp:
|
||||||
xmp_expected = fp.read()
|
xmp_expected = fp.read()
|
||||||
|
|
||||||
xmp_got = photo._xmp_sidecar(
|
xmp_got = PhotoExporter(photo)._xmp_sidecar(
|
||||||
keyword_template=["{created.year}", "{folder_album}"], extension="jpg"
|
keyword_template=["{created.year}", "{folder_album}"], extension="jpg"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
""" test ExportResults class """
|
""" test ExportResults class """
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from osxphotos.photoinfo import ExportResults
|
from osxphotos.photoexporter import ExportResults
|
||||||
|
|
||||||
EXPORT_RESULT_ATTRIBUTES = [
|
EXPORT_RESULT_ATTRIBUTES = [
|
||||||
"exported",
|
"exported",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import pytest
|
|||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
from osxphotos._constants import _UNKNOWN_PERSON
|
from osxphotos._constants import _UNKNOWN_PERSON
|
||||||
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
from osxphotos.utils import _get_os_version
|
from osxphotos.utils import _get_os_version
|
||||||
|
|
||||||
OS_VERSION = _get_os_version()
|
OS_VERSION = _get_os_version()
|
||||||
@@ -1109,6 +1110,7 @@ def test_date_invalid():
|
|||||||
"""Test date is invalid"""
|
"""Test date is invalid"""
|
||||||
# doesn't run correctly with the module-level fixture
|
# doesn't run correctly with the module-level fixture
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import osxphotos
|
import osxphotos
|
||||||
|
|
||||||
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
photosdb = osxphotos.PhotosDB(dbfile=PHOTOS_DB)
|
||||||
@@ -1349,7 +1351,7 @@ def test_exiftool_newlines_in_description(photosdb):
|
|||||||
"""Test that exiftool handles newlines embedded in description, issue #393"""
|
"""Test that exiftool handles newlines embedded in description, issue #393"""
|
||||||
|
|
||||||
photo = photosdb.get_photo(UUID_DICT["description_newlines"])
|
photo = photosdb.get_photo(UUID_DICT["description_newlines"])
|
||||||
exif = photo._exiftool_dict()
|
exif = PhotoExporter(photo)._exiftool_dict()
|
||||||
assert photo.description.find("\n") > 0
|
assert photo.description.find("\n") > 0
|
||||||
assert exif["EXIF:ImageDescription"].find("\n") > 0
|
assert exif["EXIF:ImageDescription"].find("\n") > 0
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
from math import isclose
|
from math import isclose
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from osxphotos.photoinfo import ScoreInfo
|
from osxphotos.scoreinfo import ScoreInfo
|
||||||
|
|
||||||
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
|
PHOTOS_DB_5 = "tests/Test-10.15.5.photoslibrary"
|
||||||
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
PHOTOS_DB_4 = "tests/Test-10.14.6.photoslibrary"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import osxphotos
|
|||||||
from osxphotos._constants import SIDECAR_XMP
|
from osxphotos._constants import SIDECAR_XMP
|
||||||
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
||||||
from osxphotos.fileutil import FileUtil
|
from osxphotos.fileutil import FileUtil
|
||||||
|
from osxphotos.photoexporter import PhotoExporter
|
||||||
|
|
||||||
PHOTOS_DB_15_7 = "tests/Test-10.15.7.photoslibrary"
|
PHOTOS_DB_15_7 = "tests/Test-10.15.7.photoslibrary"
|
||||||
|
|
||||||
@@ -39,7 +40,7 @@ def test_sidecar_xmp(photosdb):
|
|||||||
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos")
|
tempdir = tempfile.TemporaryDirectory(prefix="osxphotos")
|
||||||
dest = tempdir.name
|
dest = tempdir.name
|
||||||
photo = photosdb.get_photo(uuid)
|
photo = photosdb.get_photo(uuid)
|
||||||
photo.export2(dest, photo.original_filename, sidecar=SIDECAR_XMP)
|
PhotoExporter(photo).export2(dest, photo.original_filename, sidecar=SIDECAR_XMP)
|
||||||
filepath = str(pathlib.Path(dest) / photo.original_filename)
|
filepath = str(pathlib.Path(dest) / photo.original_filename)
|
||||||
xmppath = filepath + ".xmp"
|
xmppath = filepath + ".xmp"
|
||||||
|
|
||||||
@@ -53,6 +54,8 @@ def test_sidecar_xmp(photosdb):
|
|||||||
test_xmp = str(pathlib.Path(dest) / "test.xmp")
|
test_xmp = str(pathlib.Path(dest) / "test.xmp")
|
||||||
FileUtil.copy(xmppath, test_xmp)
|
FileUtil.copy(xmppath, test_xmp)
|
||||||
exif = ExifTool(test_xmp)
|
exif = ExifTool(test_xmp)
|
||||||
output, warning, error = exif.run_commands("-tagsfromfile", xmppath, "-all:all", test_xmp, no_file=True)
|
output, warning, error = exif.run_commands(
|
||||||
|
"-tagsfromfile", xmppath, "-all:all", test_xmp, no_file=True
|
||||||
|
)
|
||||||
assert not warning
|
assert not warning
|
||||||
assert not error
|
assert not error
|
||||||
|
|||||||
Reference in New Issue
Block a user