Compare commits

..

6 Commits

Author SHA1 Message Date
Rhet Turnbull
a05e7be14e Updated test data 2021-07-20 06:09:40 -07:00
Rhet Turnbull
e27c40c772 Fixed album sort order for custom sort, #497 2021-07-20 05:41:13 -07:00
Rhet Turnbull
e752f3c7a7 Updated CHANGELOG.md [skip ci] 2021-07-18 21:09:39 -07:00
Rhet Turnbull
6f4cab6721 Updated example [skip ci] 2021-07-18 20:28:56 -07:00
Rhet Turnbull
2d899ef045 Pass dest_path to template function via RenderOptions, enable implementation of #496 2021-07-18 19:42:01 -07:00
Rhet Turnbull
4f17c8fb23 Updated CHANGELOG.md [skip ci] 2021-07-18 19:38:22 -07:00
72 changed files with 675 additions and 383 deletions

View File

@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [v0.42.64](https://github.com/RhetTbull/osxphotos/compare/v0.42.63...v0.42.64)
> 18 July 2021
- Pass dest_path to template function via RenderOptions, enable implementation of #496 [`2d899ef`](https://github.com/RhetTbull/osxphotos/commit/2d899ef0453c0800ff9b9d374b2b7db0948688fe)
#### [v0.42.63](https://github.com/RhetTbull/osxphotos/compare/v0.42.62...v0.42.63)
> 18 July 2021
- Added album_sort_order example [`b04ea81`](https://github.com/RhetTbull/osxphotos/commit/b04ea8174d049d9f3783aac6bbc397ed71584965)
- Updated README.md [skip ci] [`88099de`](https://github.com/RhetTbull/osxphotos/commit/88099de688bcb6a1ddcad6c340833f1627aff268)
- Added RenderOptions to {function} template, #496 [`173a0fc`](https://github.com/RhetTbull/osxphotos/commit/173a0fce28e91177dec114d0dba001adfb76834a)
#### [v0.42.62](https://github.com/RhetTbull/osxphotos/compare/v0.42.61...v0.42.62)
> 16 July 2021

View File

@@ -1815,7 +1815,7 @@ Substitution Description
{lf} A line feed: '\n', alias for {newline}
{cr} A carriage return: '\r'
{crlf} a carriage return + line feed: '\r\n'
{osxphotos_version} The osxphotos version, e.g. '0.42.62'
{osxphotos_version} The osxphotos version, e.g. '0.42.65'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -3038,8 +3038,24 @@ Returns a list of [FolderInfo](#FolderInfo) objects representing the sub-folders
#### `parent`
Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder.
#### `sort_order`
Returns album sort order (as `AlbumSortOrder` enum). On Photos <=4, always returns `AlbumSortOrder.MANUAL`.
`AlbumSortOrder` has following values:
- `UNKNOWN`
- `MANUAL`
- `NEWEST_FIRST`
- `OLDEST_FIRST`
- `TITLE`
#### `photo_index(photo)`
Returns index of photo in album (based on album sort order).
**Note**: FolderInfo and AlbumInfo objects effectively work as a linked list. The children of a folder are contained in `subfolders` and `album_info` and the parent object of both `AlbumInfo` and `FolderInfo` is represented by `parent`. For example:
```pycon
>>> import osxphotos
>>> photosdb = osxphotos.PhotosDB()
@@ -3647,7 +3663,7 @@ The following template field substitutions are availabe for use the templating s
|{lf}|A line feed: '\n', alias for {newline}|
|{cr}|A carriage return: '\r'|
|{crlf}|a carriage return + line feed: '\r\n'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.62'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.65'|
|{osxphotos_cmd_line}|The full command line used to run osxphotos|
|{album}|Album(s) photo is contained in|
|{folder_album}|Folder path + album photo is contained in. e.g. 'Folder/Subfolder/Album' or just 'Album' if no enclosing folder|

View File

@@ -3,7 +3,7 @@
# script to help build osxphotos release
# this is unique to my own dev setup
activate osxphotos
source venv/bin/activate
rm -rf dist; rm -rf build
python3 utils/update_readme.py
(cd docsrc && make github && make pdf)

View File

@@ -1,15 +1,18 @@
""" Example function for use with osxphotos export --post-function option showing how to record album sort order """
import os
import pathlib
from typing import Optional
from osxphotos import ExportResults, PhotoInfo
from osxphotos.albuminfo import AlbumInfo
from osxphotos.path_utils import sanitize_dirname
from osxphotos.phototemplate import RenderOptions
def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
"""Get the sort order of photo in album
Returns: sort order as int or None if photo not found in album
"""
# get the album sort order from the album_info
@@ -25,6 +28,34 @@ def _get_album_sort_order(album: AlbumInfo, photo: PhotoInfo) -> Optional[int]:
return sort_order
def album_sequence(photo: PhotoInfo, options: RenderOptions, **kwargs) -> str:
"""Call this with {function} template to get album sequence (sort order) when exporting with {folder_album} template
For example, calling this template function like the following prepends sequence#_ to each exported file if the file is in an album:
osxphotos export /path/to/export -V --directory "{folder_album}" --filename "{album?{function:examples/album_sort_order.py::album_sequence}_,}{original_name}"
The sequence will start at 0. To change the sequence to start at a different offset (e.g. 1), set the environment variable OSXPHOTOS_ALBUM_SEQUENCE_START=1 (or whatever offset you want)
"""
dest_path = options.dest_path
if not dest_path:
return ""
album_info = None
for album in photo.album_info:
# following code is how {folder_album} builds the folder path
folder = "/".join(sanitize_dirname(f) for f in album.folder_names)
folder += "/" + sanitize_dirname(album.title)
if dest_path.endswith(folder):
album_info = album
break
else:
# didn't find the album, so skip this file
return ""
start_index = int(os.getenv("OSXPHOTOS_ALBUM_SEQUENCE_START", 0))
return str(album_info.photo_index(photo) + start_index)
def album_sort_order(
photo: PhotoInfo, results: ExportResults, verbose: callable, **kwargs
):
@@ -92,7 +123,7 @@ def album_sort_order(
if sort_order is None:
# didn't find the photo, so skip this file
return
verbose(f"Sort order for {filepath} in album {album_dir} is {sort_order}")
with open(str(filepath) + "_sort_order.txt", "w") as f:
f.write(str(sort_order))

View File

@@ -1,3 +1,4 @@
from ._constants import AlbumSortOrder
from ._version import __version__
from .exiftool import ExifTool
from .photoinfo import ExportResults, PhotoInfo

View File

@@ -4,6 +4,7 @@ Constants used by osxphotos
import os.path
from datetime import datetime
from enum import Enum
OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
@@ -273,3 +274,11 @@ POST_COMMAND_CATEGORIES = {
# "deleted_files": "When used with '--cleanup', all files deleted during the export",
# "deleted_directories": "When used with '--cleanup', all directories deleted during the export",
}
class AlbumSortOrder(Enum):
"""Album Sort Order"""
UNKNOWN = 0
MANUAL = 1
NEWEST_FIRST = 2
OLDEST_FIRST = 3
TITLE = 5

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.63"
__version__ = "0.42.65"

View File

@@ -19,21 +19,22 @@ from ._constants import (
_PHOTOS_5_ALBUM_KIND,
_PHOTOS_5_FOLDER_KIND,
TIME_DELTA,
AlbumSortOrder,
)
from .datetime_utils import get_local_tz
def sort_list_by_keys(values, sort_keys):
""" Sorts list values by a second list sort_keys
"""Sorts list values by a second list sort_keys
e.g. given ["a","c","b"], [1, 3, 2], returns ["a", "b", "c"]
Args:
values: a list of values to be sorted
sort_keys: a list of keys to sort values by
Returns:
list of values, sorted by sort_keys
Raises:
ValueError: raised if len(values) != len(sort_keys)
"""
@@ -63,12 +64,12 @@ class AlbumInfoBaseClass:
@property
def uuid(self):
""" return uuid of album """
"""return uuid of album"""
return self._uuid
@property
def creation_date(self):
""" return creation date of album """
"""return creation date of album"""
try:
return self._creation_date
except AttributeError:
@@ -90,8 +91,8 @@ class AlbumInfoBaseClass:
@property
def start_date(self):
""" For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began) """
"""For Albums, return start date (earliest image) of album or None for albums with no images
For Import Sessions, return start date of import session (when import began)"""
try:
return self._start_date
except AttributeError:
@@ -109,8 +110,8 @@ class AlbumInfoBaseClass:
@property
def end_date(self):
""" For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed) """
"""For Albums, return end date (most recent image) of album or None for albums with no images
For Import Sessions, return end date of import sessions (when import was completed)"""
try:
return self._end_date
except AttributeError:
@@ -131,7 +132,7 @@ class AlbumInfoBaseClass:
return []
def __len__(self):
""" return number of photos contained in album """
"""return number of photos contained in album"""
return len(self.photos)
@@ -144,29 +145,39 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def title(self):
""" return title / name of album """
"""return title / name of album"""
return self._title
@property
def photos(self):
""" return list of photos contained in album sorted in same sort order as Photos """
"""return list of photos contained in album sorted in same sort order as Photos"""
try:
return self._photos
except AttributeError:
if self.uuid in self._db._dbalbums_album:
uuid, sort_order = zip(*self._db._dbalbums_album[self.uuid])
sorted_uuid = sort_list_by_keys(uuid, sort_order)
self._photos = self._db.photos_by_uuid(sorted_uuid)
photos = self._db.photos_by_uuid(sorted_uuid)
sort_order = self.sort_order
if sort_order == AlbumSortOrder.NEWEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date, reverse=True)
elif sort_order == AlbumSortOrder.OLDEST_FIRST:
self._photos = sorted(photos, key=lambda p: p.date)
elif sort_order == AlbumSortOrder.TITLE:
self._photos = sorted(photos, key=lambda p: p.title or "")
else:
# assume AlbumSortOrder.MANUAL
self._photos = photos
else:
self._photos = []
return self._photos
@property
def folder_names(self):
""" return hierarchical list of folders the album is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
"""return hierarchical list of folders the album is contained in
the folder list is in form:
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
try:
return self._folder_names
@@ -176,10 +187,10 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def folder_list(self):
""" return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders """
"""return hierarchical list of folders the album is contained in
as list of FolderInfo objects in form
["Top level folder", "sub folder 1", "sub folder 2", ...]
returns empty list if album is not in any folders"""
try:
return self._folders
@@ -189,7 +200,7 @@ class AlbumInfo(AlbumInfoBaseClass):
@property
def parent(self):
""" returns FolderInfo object for parent folder or None if no parent (e.g. top-level album) """
"""returns FolderInfo object for parent folder or None if no parent (e.g. top-level album)"""
try:
return self._parent
except AttributeError:
@@ -209,11 +220,44 @@ class AlbumInfo(AlbumInfoBaseClass):
)
return self._parent
@property
def sort_order(self):
"""return sort order of album"""
if self._db._db_version <= _PHOTOS_4_VERSION:
return AlbumSortOrder.MANUAL
details = self._db._dbalbum_details[self._uuid]
if details["customsortkey"] == 1:
if details["customsortascending"] == 0:
return AlbumSortOrder.NEWEST_FIRST
elif details["customsortascending"] == 1:
return AlbumSortOrder.OLDEST_FIRST
else:
return AlbumSortOrder.UNKNOWN
elif details["customsortkey"] == 5:
return AlbumSortOrder.TITLE
elif details["customsortkey"] == 0:
return AlbumSortOrder.MANUAL
else:
return AlbumSortOrder.UNKNOWN
def photo_index(self, photo):
"""return index of photo in album (based on album sort order)"""
index = 0
for p in self.photos:
if p.uuid == photo.uuid:
return index
index += 1
else:
raise ValueError(
f"Photo with uuid {photo.uuid} does not appear to be in this album"
)
class ImportInfo(AlbumInfoBaseClass):
@property
def photos(self):
""" return list of photos contained in import session """
"""return list of photos contained in import session"""
try:
return self._photos
except AttributeError:
@@ -231,7 +275,7 @@ class ImportInfo(AlbumInfoBaseClass):
class FolderInfo:
"""
Info about a specific folder, contains all the details about the folder
Info about a specific folder, contains all the details about the folder
including folders, albums, etc
"""
@@ -247,17 +291,17 @@ class FolderInfo:
@property
def title(self):
""" return title / name of folder"""
"""return title / name of folder"""
return self._title
@property
def uuid(self):
""" return uuid of folder """
"""return uuid of folder"""
return self._uuid
@property
def album_info(self):
""" return list of albums (as AlbumInfo objects) contained in the folder """
"""return list of albums (as AlbumInfo objects) contained in the folder"""
try:
return self._albums
except AttributeError:
@@ -282,7 +326,7 @@ class FolderInfo:
@property
def parent(self):
""" returns FolderInfo object for parent or None if no parent (e.g. top-level folder) """
"""returns FolderInfo object for parent or None if no parent (e.g. top-level folder)"""
try:
return self._parent
except AttributeError:
@@ -304,7 +348,7 @@ class FolderInfo:
@property
def subfolders(self):
""" return list of folders (as FolderInfo objects) contained in the folder """
"""return list of folders (as FolderInfo objects) contained in the folder"""
try:
return self._folders
except AttributeError:
@@ -328,5 +372,5 @@ class FolderInfo:
return self._folders
def __len__(self):
""" returns count of folders + albums contained in the folder """
"""returns count of folders + albums contained in the folder"""
return len(self.subfolders) + len(self.album_info)

View File

@@ -2556,133 +2556,52 @@ def export_photo(
)
results = ExportResults()
filenames = get_filenames_from_template(
photo, filename_template, original_name, strip=strip
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=False
)
for filename in filenames:
original_filename = pathlib.Path(filename)
file_ext = original_filename.suffix
if photo.isphoto and (jpeg_ext or convert_to_jpeg):
# change the file extension to correct jpeg extension if needed
file_ext = (
"." + jpeg_ext
if jpeg_ext and (photo.uti_original == "public.jpeg" or convert_to_jpeg)
else ".jpeg"
if convert_to_jpeg and photo.uti_original != "public.jpeg"
else original_filename.suffix
)
original_filename = (
original_filename.parent
/ f"{original_filename.stem}{rendered_suffix}{file_ext}"
)
original_filename = str(original_filename)
verbose_(
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
for dest_path in dest_paths:
filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip
)
results += export_photo_with_template(
photo=photo,
filename=original_filename,
directory=directory,
edited=False,
use_photos_export=use_photos_export,
export_by_date=export_by_date,
dest=dest,
dry_run=dry_run,
strip=strip,
export_original=export_original,
missing=missing_original,
verbose=verbose,
sidecar_flags=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
export_live=export_live,
export_raw=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
exiftool=exiftool,
exiftool_merge_keywords=exiftool_merge_keywords,
exiftool_merge_persons=exiftool_merge_persons,
album_keyword=album_keyword,
person_keyword=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
exiftool_option=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
export_dir=export_dir,
export_preview=export_preview,
preview_suffix=rendered_preview_suffix,
preview_if_missing=preview_if_missing,
)
if export_edited and photo.hasadjustments:
# if export-edited, also export the edited version
edited_filenames = get_filenames_from_template(
photo, filename_template, original_name, strip=strip, edited=True
)
for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename)
# verify the photo has adjustments and valid path to avoid raising an exception
edited_ext = (
# rare cases on Photos <= 4 that uti_edited is None
"." + get_preferred_uti_extension(photo.uti_edited)
if photo.uti_edited
else pathlib.Path(photo.path_edited).suffix
if photo.path_edited
else pathlib.Path(photo.filename).suffix
)
if photo.isphoto and jpeg_ext and edited_ext.lower() in [".jpg", ".jpeg"]:
edited_ext = "." + jpeg_ext
# Big Sur uses .heic for some edited photos so need to check
# if extension isn't jpeg/jpg and using --convert-to-jpeg
if (
photo.isphoto
and convert_to_jpeg
and edited_ext.lower() not in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
rendered_edited_suffix = _render_suffix_template(
edited_suffix, "edited_suffix", "--edited-suffix", strip, dest, photo
)
edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
for filename in filenames:
original_filename = pathlib.Path(filename)
file_ext = original_filename.suffix
if photo.isphoto and (jpeg_ext or convert_to_jpeg):
# change the file extension to correct jpeg extension if needed
file_ext = (
"." + jpeg_ext
if jpeg_ext
and (photo.uti_original == "public.jpeg" or convert_to_jpeg)
else ".jpeg"
if convert_to_jpeg and photo.uti_original != "public.jpeg"
else original_filename.suffix
)
original_filename = (
original_filename.parent
/ f"{original_filename.stem}{rendered_suffix}{file_ext}"
)
original_filename = str(original_filename)
verbose_(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
f"Exporting {photo.original_filename} ({photo.filename}) as {original_filename}"
)
results += export_photo_with_template(
results += export_photo_to_directory(
photo=photo,
filename=edited_filename,
directory=directory,
edited=True,
filename=original_filename,
dest_path=dest_path,
edited=False,
use_photos_export=use_photos_export,
export_by_date=export_by_date,
dest=dest,
dry_run=dry_run,
strip=strip,
export_original=False,
missing=missing_edited,
export_original=export_original,
missing=missing_original,
verbose=verbose,
sidecar_flags=sidecar_flags if not export_original else 0,
sidecar_flags=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
export_live=export_live,
export_raw=not export_original and export_raw,
export_raw=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
exiftool=exiftool,
@@ -2706,11 +2625,107 @@ def export_photo(
replace_keywords=replace_keywords,
retry=retry,
export_dir=export_dir,
export_preview=not export_original and export_preview,
export_preview=export_preview,
preview_suffix=rendered_preview_suffix,
preview_if_missing=preview_if_missing,
)
if export_edited and photo.hasadjustments:
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=True
)
for dest_path in dest_paths:
# if export-edited, also export the edited version
edited_filenames = get_filenames_from_template(
photo, filename_template, dest, dest_path, original_name, strip=strip, edited=True
)
for edited_filename in edited_filenames:
edited_filename = pathlib.Path(edited_filename)
# verify the photo has adjustments and valid path to avoid raising an exception
edited_ext = (
# rare cases on Photos <= 4 that uti_edited is None
"." + get_preferred_uti_extension(photo.uti_edited)
if photo.uti_edited
else pathlib.Path(photo.path_edited).suffix
if photo.path_edited
else pathlib.Path(photo.filename).suffix
)
if (
photo.isphoto
and jpeg_ext
and edited_ext.lower() in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext
# Big Sur uses .heic for some edited photos so need to check
# if extension isn't jpeg/jpg and using --convert-to-jpeg
if (
photo.isphoto
and convert_to_jpeg
and edited_ext.lower() not in [".jpg", ".jpeg"]
):
edited_ext = "." + jpeg_ext if jpeg_ext else ".jpeg"
rendered_edited_suffix = _render_suffix_template(
edited_suffix,
"edited_suffix",
"--edited-suffix",
strip,
dest,
photo,
)
edited_filename = (
f"{edited_filename.stem}{rendered_edited_suffix}{edited_ext}"
)
verbose_(
f"Exporting edited version of {photo.original_filename} ({photo.filename}) as {edited_filename}"
)
results += export_photo_to_directory(
photo=photo,
filename=edited_filename,
dest_path=dest_path,
edited=True,
use_photos_export=use_photos_export,
dest=dest,
dry_run=dry_run,
export_original=False,
missing=missing_edited,
verbose=verbose,
sidecar_flags=sidecar_flags if not export_original else 0,
sidecar_drop_ext=sidecar_drop_ext,
export_live=export_live,
export_raw=not export_original and export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
exiftool=exiftool,
exiftool_merge_keywords=exiftool_merge_keywords,
exiftool_merge_persons=exiftool_merge_persons,
album_keyword=album_keyword,
person_keyword=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
exiftool_option=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
retry=retry,
export_dir=export_dir,
export_preview=not export_original and export_preview,
preview_suffix=rendered_preview_suffix,
preview_if_missing=preview_if_missing,
)
return results
@@ -2744,16 +2759,14 @@ def _render_suffix_template(suffix_template, var_name, option_name, strip, dest,
return rendered_suffix[0]
def export_photo_with_template(
def export_photo_to_directory(
photo,
filename,
directory,
dest_path,
edited,
use_photos_export,
export_by_date,
dest,
dry_run,
strip,
export_original,
missing,
verbose,
@@ -2788,159 +2801,153 @@ def export_photo_with_template(
preview_suffix,
preview_if_missing,
):
"""Evaluate directory template then export photo to each directory"""
"""Export photo to directory dest_path"""
results = ExportResults()
if export_original:
if missing and not preview_if_missing:
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
elif (
photo.intrash
and (not photo.path or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
return results
elif not edited:
verbose_(f"Skipping original version of {photo.original_filename}")
return results
else:
# exporting the edited version
if missing and not preview_if_missing:
space = " " if not verbose else ""
verbose_(f"{space}Skipping missing edited photo for {filename}")
results.missing.append(str(pathlib.Path(dest_path) / filename))
return results
elif (
photo.intrash
and (not photo.path_edited or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
return results
dest_paths = get_dirnames_from_template(
photo, directory, export_by_date, dest, dry_run, strip=strip, edited=edited
)
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
# export the photo to each path in dest_paths
for dest_path in dest_paths:
if export_original:
if missing and not preview_if_missing:
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
elif (
photo.intrash
and (not photo.path or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
continue
elif not edited:
verbose_(f"Skipping original version of {photo.original_filename}")
continue
else:
# exporting the edited version
if missing and not preview_if_missing:
space = " " if not verbose else ""
verbose_(f"{space}Skipping missing edited photo for {filename}")
results.missing.append(str(pathlib.Path(dest_path) / filename))
continue
elif (
photo.intrash
and (not photo.path_edited or use_photos_export)
and not preview_if_missing
):
# skip deleted files if they're missing or using use_photos_export
# as AppleScript/PhotoKit cannot export deleted photos
space = " " if not verbose else ""
verbose_(
f"{space}Skipping missing deleted photo {photo.original_filename} ({photo.uuid})"
)
results.missing.append(str(pathlib.Path(dest_path) / filename))
continue
render_options = RenderOptions(export_dir=export_dir, dest_path=dest_path)
tries = 0
while tries <= retry:
tries += 1
error = 0
try:
export_results = photo.export2(
dest_path,
original_filename=filename,
edited=edited,
original=export_original,
edited_filename=filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
render_options=render_options,
preview=export_preview or (missing and preview_if_missing),
preview_suffix=preview_suffix,
)
for warning_ in export_results.exiftool_warning:
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
tries = 0
while tries <= retry:
tries += 1
error = 0
try:
export_results = photo.export2(
dest_path,
original_filename=filename,
edited=edited,
original=export_original,
edited_filename=filename,
sidecar=sidecar_flags,
sidecar_drop_ext=sidecar_drop_ext,
live_photo=export_live,
raw_photo=export_raw,
export_as_hardlink=export_as_hardlink,
overwrite=overwrite,
use_photos_export=use_photos_export,
exiftool=exiftool,
merge_exif_keywords=exiftool_merge_keywords,
merge_exif_persons=exiftool_merge_persons,
use_albums_as_keywords=album_keyword,
use_persons_as_keywords=person_keyword,
keyword_template=keyword_template,
description_template=description_template,
update=update,
ignore_signature=ignore_signature,
export_db=export_db,
fileutil=fileutil,
dry_run=dry_run,
touch_file=touch_file,
convert_to_jpeg=convert_to_jpeg,
jpeg_quality=jpeg_quality,
ignore_date_modified=ignore_date_modified,
use_photokit=use_photokit,
verbose=verbose_,
exiftool_flags=exiftool_option,
jpeg_ext=jpeg_ext,
replace_keywords=replace_keywords,
render_options=render_options,
preview=export_preview or (missing and preview_if_missing),
preview_suffix=preview_suffix,
)
for warning_ in export_results.exiftool_warning:
verbose_(f"exiftool warning for file {warning_[0]}: {warning_[1]}")
for error_ in export_results.exiftool_error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {filename}: {e}",
f"exiftool error for file {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
if tries > retry:
results.error.append((str(pathlib.Path(dest) / filename), e))
break
else:
click.echo(
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
if verbose:
if update:
for new in results.new:
verbose_(f"Exported new file {new}")
for updated in results.updated:
verbose_(f"Exported updated file {updated}")
for skipped in results.skipped:
verbose_(f"Skipped up to date file {skipped}")
for error_ in export_results.error:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {error_[0]}: {error_[1]}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
error += 1
if not error or tries > retry:
results += export_results
break
else:
for exported in results.exported:
verbose_(f"Exported {exported}")
for touched in results.touched:
verbose_(f"Touched date on file {touched}")
click.echo(
"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
except Exception as e:
click.echo(
click.style(
f"Error exporting photo ({photo.uuid}: {photo.original_filename}) as {filename}: {e}",
fg=CLI_COLOR_ERROR,
),
err=True,
)
if tries > retry:
results.error.append((str(pathlib.Path(dest) / filename), e))
break
else:
click.echo(
f"Retrying export for photo ({photo.uuid}: {photo.original_filename})"
)
if verbose:
if update:
for new in results.new:
verbose_(f"Exported new file {new}")
for updated in results.updated:
verbose_(f"Exported updated file {updated}")
for skipped in results.skipped:
verbose_(f"Skipped up to date file {skipped}")
else:
for exported in results.exported:
verbose_(f"Exported {exported}")
for touched in results.touched:
verbose_(f"Touched date on file {touched}")
return results
@@ -2948,6 +2955,8 @@ def export_photo_with_template(
def get_filenames_from_template(
photo,
filename_template,
export_dir,
dest_path,
original_name,
strip=False,
edited=False,
@@ -2958,6 +2967,7 @@ def get_filenames_from_template(
photo: a PhotoInfo instance
filename_template: a PhotoTemplate template string, may be None
original_name: boolean; if True, use photo's original filename instead of current filename
dest_path: the path the photo will be exported to
strip: if True, strips leading/trailing white space from resulting template
edited: if True, sets {edited_version} field to True, otherwise it gets set to False; set if you want template evaluated for edited version
@@ -2975,6 +2985,8 @@ def get_filenames_from_template(
filename=True,
strip=strip,
edited_version=edited,
export_dir=export_dir,
dest_path=dest_path,
)
filenames, unmatched = photo.render_template(filename_template, options)
except ValueError as e:

View File

@@ -790,6 +790,8 @@ class PhotosDB:
"creation_date": album[8],
"start_date": None, # Photos 5 only
"end_date": None, # Photos 5 only
"customsortascending": None, # Photos 5 only
"customsortkey": None, # Photos 5 only
}
# get details about folders
@@ -1767,7 +1769,9 @@ class PhotosDB:
"ZTRASHEDSTATE, " # 9
"ZCREATIONDATE, " # 10
"ZSTARTDATE, " # 11
"ZENDDATE " # 12
"ZENDDATE, " # 12
"ZCUSTOMSORTASCENDING, " # 13
"ZCUSTOMSORTKEY " # 14
"FROM ZGENERICALBUM "
)
for album in c:
@@ -1787,6 +1791,8 @@ class PhotosDB:
"creation_date": album[10],
"start_date": album[11],
"end_date": album[12],
"customsortascending": album[13],
"customsortkey": album[14],
}
# add cross-reference by pk to uuid

View File

@@ -7,7 +7,7 @@
<key>hostuuid</key>
<string>585B80BF-8D1F-55EF-A9E8-6CF4E5523959</string>
<key>pid</key>
<integer>570</integer>
<integer>1961</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>

View File

@@ -3,24 +3,24 @@
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T05:48:01Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2021-03-13T16:38:24Z</date>
<date>2021-07-20T05:48:00Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T07:05:31Z</date>
<key>BackgroundJobSearch</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T05:48:01Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2021-03-13T16:38:23Z</date>
<date>2021-07-20T05:48:00Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T05:48:01Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-10-17T23:45:33Z</date>
<date>2021-07-20T05:48:08Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-17T23:45:24Z</date>
<date>2021-07-20T05:47:59Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T05:48:01Z</date>
<key>SiriPortraitDonation</key>
<date>2021-03-13T16:38:25Z</date>
<date>2021-07-20T05:48:01Z</date>
</dict>
</plist>

View File

@@ -3,8 +3,8 @@
<plist version="1.0">
<dict>
<key>FaceIDModelLastGenerationKey</key>
<date>2020-10-17T23:45:32Z</date>
<date>2021-07-20T05:48:02Z</date>
<key>LastContactClassificationKey</key>
<date>2020-10-17T23:45:54Z</date>
<date>2021-07-20T05:48:05Z</date>
</dict>
</plist>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2020-04-11T19:26:12Z</date>
<date>2021-07-20T12:21:20Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2020-05-29T15:20:05Z</date>
<date>2021-07-20T12:21:21Z</date>
<key>coalescePayloadVersion</key>
<integer>10</integer>
<key>currentPayloadVersion</key>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2019-10-27T15:36:05Z</date>
<date>2021-07-20T12:21:20Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2020-05-29T15:20:05Z</date>
<date>2021-07-20T12:21:21Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>

View File

@@ -3,7 +3,7 @@
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2020-05-29T15:20:05Z</date>
<date>2021-07-20T12:21:20Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>

View File

@@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>coalesceDate</key>
<date>2021-07-20T12:21:20Z</date>
<key>coalescePayloadVersion</key>
<integer>1</integer>
<key>currentPayloadVersion</key>
<integer>1</integer>
<key>snapshotDate</key>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

View File

@@ -19,6 +19,10 @@
<rdf:Bag>
<rdf:li>Kids</rdf:li>
<rdf:li>Pumpkin Farm</rdf:li>
<rdf:li>Sorted Manual</rdf:li>
<rdf:li>Sorted Newest First</rdf:li>
<rdf:li>Sorted Oldest First</rdf:li>
<rdf:li>Sorted Title</rdf:li>
<rdf:li>Test Album</rdf:li>
</rdf:Bag>
</dc:subject>
@@ -39,6 +43,10 @@
<rdf:Seq>
<rdf:li>Kids</rdf:li>
<rdf:li>Pumpkin Farm</rdf:li>
<rdf:li>Sorted Manual</rdf:li>
<rdf:li>Sorted Newest First</rdf:li>
<rdf:li>Sorted Oldest First</rdf:li>
<rdf:li>Sorted Title</rdf:li>
<rdf:li>Test Album</rdf:li>
</rdf:Seq>
</digiKam:TagsList>

View File

@@ -1 +1 @@
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]
[{"EXIF:ImageDescription": "Girls with pumpkins", "XMP:Description": "Girls with pumpkins", "IPTC:Caption-Abstract": "Girls with pumpkins", "XMP:Title": "Can we carry this?", "IPTC:ObjectName": "Can we carry this?", "IPTC:Keywords": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:Subject": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:TagsList": ["Kids", "Pumpkin Farm", "Sorted Manual", "Sorted Newest First", "Sorted Oldest First", "Sorted Title", "Test Album"], "XMP:PersonInImage": ["Katie", "Suzy"], "EXIF:DateTimeOriginal": "2018:09:28 15:35:49", "EXIF:CreateDate": "2018:09:28 15:35:49", "EXIF:OffsetTimeOriginal": "-04:00", "IPTC:DateCreated": "2018:09:28", "IPTC:TimeCreated": "15:35:49-04:00", "EXIF:ModifyDate": "2018:09:28 15:35:49"}]

View File

@@ -20,6 +20,10 @@
<rdf:li>2018</rdf:li>
<rdf:li>Kids</rdf:li>
<rdf:li>Pumpkin Farm</rdf:li>
<rdf:li>Sorted Manual</rdf:li>
<rdf:li>Sorted Newest First</rdf:li>
<rdf:li>Sorted Oldest First</rdf:li>
<rdf:li>Sorted Title</rdf:li>
<rdf:li>Test Album</rdf:li>
</rdf:Bag>
</dc:subject>
@@ -41,6 +45,10 @@
<rdf:li>2018</rdf:li>
<rdf:li>Kids</rdf:li>
<rdf:li>Pumpkin Farm</rdf:li>
<rdf:li>Sorted Manual</rdf:li>
<rdf:li>Sorted Newest First</rdf:li>
<rdf:li>Sorted Oldest First</rdf:li>
<rdf:li>Sorted Title</rdf:li>
<rdf:li>Test Album</rdf:li>
</rdf:Seq>
</digiKam:TagsList>

View File

@@ -2,11 +2,11 @@ import pytest
import osxphotos
from osxphotos._constants import _UNKNOWN_PERSON
from osxphotos._constants import _UNKNOWN_PERSON, AlbumSortOrder
PHOTOS_DB = "./tests/Test-10.15.4.photoslibrary/database/photos.db"
PHOTOS_DB = "./tests/Test-10.15.7.photoslibrary/database/photos.db"
TOP_LEVEL_FOLDERS = ["Folder1", "Folder2"]
TOP_LEVEL_FOLDERS = ["Folder1", "Folder2", "Pumpkin Farm"]
TOP_LEVEL_CHILDREN = ["SubFolder1", "SubFolder2"]
@@ -15,25 +15,73 @@ FOLDER_ALBUM_DICT = {
"SubFolder1": [],
"SubFolder2": ["AlbumInFolder"],
"Folder2": ["Raw"],
"Pumpkin Farm": [],
}
ALBUM_NAMES = ["Pumpkin Farm", "AlbumInFolder", "Test Album", "Test Album", "Raw"]
ALBUM_NAMES = [
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
"2019-10/11 Paris Clermont",
"AlbumInFolder",
"EmptyAlbum",
"I have a deleted twin",
"Multi Keyword",
"Pumpkin Farm",
"Raw",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
"Test Album",
"Test Album",
]
ALBUM_PARENT_DICT = {
"Pumpkin Farm": None,
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum": None,
"2019-10/11 Paris Clermont": None,
"AlbumInFolder": "SubFolder2",
"Test Album": None,
"EmptyAlbum": None,
"I have a deleted twin": None,
"Multi Keyword": None,
"Pumpkin Farm": None,
"Raw": "Folder2",
"Sorted Manual": None,
"Sorted Newest First": None,
"Sorted Oldest First": None,
"Sorted Title": None,
"Test Album": None,
}
ALBUM_FOLDER_NAMES_DICT = {
"Pumpkin Farm": [],
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum": [],
"2019-10/11 Paris Clermont": [],
"AlbumInFolder": ["Folder1", "SubFolder2"],
"Test Album": [],
"EmptyAlbum": [],
"I have a deleted twin": [],
"Multi Keyword": [],
"Pumpkin Farm": [],
"Raw": ["Folder2"],
"Sorted Manual": [],
"Sorted Newest First": [],
"Sorted Oldest First": [],
"Sorted Title": [],
"Test Album": [],
}
ALBUM_LEN_DICT = {"Pumpkin Farm": 3, "AlbumInFolder": 2, "Test Album": 1, "Raw": 4}
ALBUM_LEN_DICT = {
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum": 1,
"2019-10/11 Paris Clermont": 1,
"AlbumInFolder": 2,
"EmptyAlbum": 0,
"I have a deleted twin": 1,
"Multi Keyword": 2,
"Pumpkin Farm": 3,
"Raw": 4,
"Sorted Manual": 3,
"Sorted Newest First": 3,
"Sorted Oldest First": 3,
"Sorted Title": 3,
"Test Album": 1,
}
ALBUM_PHOTO_UUID_DICT = {
"Pumpkin Farm": [
@@ -58,10 +106,33 @@ ALBUM_PHOTO_UUID_DICT = {
}
UUID_DICT = {
"two_albums": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
"six_albums": "F12384F6-CD17-4151-ACBA-AE0E3688539E",
"album_dates": "0C514A98-7B77-4E4F-801B-364B7B65EAFA",
}
UUID_DICT_SORT_ORDER = {
AlbumSortOrder.MANUAL: [
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
],
AlbumSortOrder.NEWEST_FIRST: [
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
],
AlbumSortOrder.OLDEST_FIRST: [
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
],
AlbumSortOrder.TITLE: [
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
"F12384F6-CD17-4151-ACBA-AE0E3688539E",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
],
}
@pytest.fixture(scope="module")
def photosdb():
@@ -186,8 +257,9 @@ def test_albums_photos(photosdb):
photos = album.photos
assert len(photos) == ALBUM_LEN_DICT[album.title]
assert len(photos) == len(album)
for photo in photos:
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
if album.title in ALBUM_PHOTO_UUID_DICT:
for photo in photos:
assert photo.uuid in ALBUM_PHOTO_UUID_DICT[album.title]
def test_album_dates(photosdb):
@@ -237,19 +309,67 @@ def test_photoinfo_albums(photosdb):
def test_photoinfo_albums_2(photosdb):
"""Test that PhotoInfo.albums returns only number albums expected"""
photos = photosdb.photos(uuid=[UUID_DICT["two_albums"]])
photos = photosdb.photos(uuid=[UUID_DICT["six_albums"]])
albums = photos[0].albums
assert len(albums) == 2
assert len(albums) == 6
def test_photoinfo_album_info(photosdb):
"""test PhotoInfo.album_info"""
photos = photosdb.photos(uuid=[UUID_DICT["two_albums"]])
photos = photosdb.photos(uuid=[UUID_DICT["six_albums"]])
album_info = photos[0].album_info
assert len(album_info) == 2
assert album_info[0].title in ["Pumpkin Farm", "Test Album"]
assert album_info[1].title in ["Pumpkin Farm", "Test Album"]
assert len(album_info) == 6
assert album_info[0].title in [
"Pumpkin Farm",
"Test Album",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
]
assert album_info[1].title in [
"Pumpkin Farm",
"Test Album",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
]
assert photos[0].uuid in [photo.uuid for photo in album_info[0].photos]
def test_album_sort_order(photosdb):
"""Test that AlbumInfo.sort_order is set correctly"""
albums = photosdb.album_info
for album in albums:
if album.title == "Sorted Manual":
assert album.sort_order == AlbumSortOrder.MANUAL
elif album.title == "Sorted Newest First":
assert album.sort_order == AlbumSortOrder.NEWEST_FIRST
elif album.title == "Sorted Oldest First":
assert album.sort_order == AlbumSortOrder.OLDEST_FIRST
elif album.title == "Sorted Title":
assert album.sort_order == AlbumSortOrder.TITLE
def test_album_sort_order_photos(photosdb):
"""Test AlbumInfo.photos returns photos sorted according to AlbumInfo.sort_order"""
albums = photosdb.album_info
for album in albums:
uuids = [photo.uuid for photo in album.photos]
if album.title == "Sorted Manual":
assert album.sort_order == AlbumSortOrder.MANUAL
assert uuids == UUID_DICT_SORT_ORDER[AlbumSortOrder.MANUAL]
if album.title == "Sorted Newest First":
assert album.sort_order == AlbumSortOrder.NEWEST_FIRST
assert uuids == UUID_DICT_SORT_ORDER[AlbumSortOrder.NEWEST_FIRST]
if album.title == "Sorted Oldest First":
assert album.sort_order == AlbumSortOrder.OLDEST_FIRST
assert uuids == UUID_DICT_SORT_ORDER[AlbumSortOrder.OLDEST_FIRST]
if album.title == "Sorted Title":
assert album.sort_order == AlbumSortOrder.TITLE
assert uuids == UUID_DICT_SORT_ORDER[AlbumSortOrder.TITLE]

View File

@@ -49,45 +49,53 @@ KEYWORDS = [
# Photos 5 includes blank person for detected face
PERSONS = ["Katie", "Suzy", "Maria", _UNKNOWN_PERSON]
ALBUMS = [
"Pumpkin Farm",
"Test Album", # there are 2 albums named "Test Album" for testing duplicate album names
"AlbumInFolder",
"Raw",
"I have a deleted twin", # there's an empty album with same name that has been deleted
"EmptyAlbum",
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
"2019-10/11 Paris Clermont",
"AlbumInFolder",
"EmptyAlbum",
"I have a deleted twin", # there's an empty album with same name that has been deleted
"Multi Keyword",
"Pumpkin Farm",
"Raw",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
"Test Album", # there are 2 albums named "Test Album" for testing duplicate album names
]
KEYWORDS_DICT = {
"Kids": 4,
"wedding": 3,
"flowers": 1,
"Drink": 2,
"England": 1,
"London": 1,
"Kids": 4,
"London 2018": 1,
"London": 1,
"Maria": 1,
"St. James's Park": 1,
"Travel": 2,
"UK": 1,
"United Kingdom": 1,
"foo/bar": 1,
"Travel": 2,
"Maria": 1,
"Drink": 2,
"Val d'Isère": 2,
"Wine": 2,
"Wine Bottle": 2,
"Wine": 2,
"flowers": 1,
"foo/bar": 1,
"wedding": 3,
}
PERSONS_DICT = {"Katie": 3, "Suzy": 2, "Maria": 2, _UNKNOWN_PERSON: 1}
ALBUM_DICT = {
"Pumpkin Farm": 3,
"Test Album": 2,
"AlbumInFolder": 2,
"Raw": 4,
"I have a deleted twin": 1,
"EmptyAlbum": 0,
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum": 1,
"2019-10/11 Paris Clermont": 1,
"AlbumInFolder": 2,
"EmptyAlbum": 0,
"I have a deleted twin": 1,
"Multi Keyword": 2,
"Pumpkin Farm": 3,
"Raw": 4,
"Sorted Manual": 3,
"Sorted Newest First": 3,
"Sorted Oldest First": 3,
"Sorted Title": 3,
"Test Album": 2,
} # Note: there are 2 albums named "Test Album" for testing duplicate album names
UUID_DICT = {

View File

@@ -608,31 +608,22 @@ CLI_FINDER_TAGS = {
LABELS_JSON = {
"labels": {
"Outdoor": 3,
"Sky": 2,
"Land": 2,
"Plant": 2,
"Art": 2,
"Food": 2,
"People": 2,
"Cloudy": 1,
"Grass": 1,
"Flower": 1,
"Container": 1,
"Water": 2,
"Underwater": 2,
"Jellyfish": 2,
"Animal": 2,
"Wine Bottle": 2,
"Drink": 2,
"Wine": 2,
"Vase": 1,
"Flower": 1,
"Plant": 1,
"Flower Arrangement": 1,
"Bouquet": 1,
"Art": 1,
"Container": 1,
"Camera": 1,
"Blue Sky": 1,
"Fruit": 1,
"Apricot": 1,
"Foliage": 1,
"Pumpkin": 1,
"Child": 1,
"Vegetable": 1,
"Recreation": 1,
"Clothing": 1,
"Adult": 1,
"Document": 1,
}
}
@@ -668,6 +659,10 @@ ALBUMS_JSON = {
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum": 1,
"2019-10/11 Paris Clermont": 1,
"EmptyAlbum": 0,
"Sorted Manual": 3,
"Sorted Newest First": 3,
"Sorted Oldest First": 3,
"Sorted Title": 3,
},
"shared albums": {},
}
@@ -754,11 +749,11 @@ UUID_IN_ALBUM = [
"4D521201-92AC-43E5-8F7C-59BC41C37A96",
"D05A5FE3-15FB-49A1-A15D-AB3DA6F8B068",
"3DD2C897-F19E-4CA6-8C22-B027D5A71907",
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
]
UUID_NOT_IN_ALBUM = [
"A1DD1F98-2ECD-431F-9AC9-5AFEFE2D3A5C",
"7783E8E6-9CAC-40F3-BE22-81FB7051C266",
"DC99FBDD-7A52-4100-A5BB-344131646C30",
"D1359D09-1373-4F3B-B0E3-1A4DE573E4A3",
"E2078879-A29C-4D6F-BACB-E3BBE6C3EB91",
@@ -2649,7 +2644,7 @@ def test_query_label_4():
)
assert result.exit_code == 0
json_got = json.loads(result.output)
assert len(json_got) == 2
assert len(json_got) == 1
def test_query_deleted_deleted_only():
@@ -2883,11 +2878,11 @@ def test_export_sidecar_templates():
exifdata = json.load(jsonfile)
assert (
exifdata[0]["XMP:Description"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Sorted Manual, Sorted Newest First, Sorted Oldest First, Sorted Title, Test Album"
)
assert (
exifdata[0]["EXIF:ImageDescription"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Sorted Manual, Sorted Newest First, Sorted Oldest First, Sorted Title, Test Album"
)
@@ -2926,11 +2921,11 @@ def test_export_sidecar_templates_exiftool():
exifdata = json.load(jsonfile)
assert (
exifdata[0]["Description"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Sorted Manual, Sorted Newest First, Sorted Oldest First, Sorted Title, Test Album"
)
assert (
exifdata[0]["ImageDescription"]
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Test Album"
== "Girls with pumpkins Katie, Suzy Kids Pumpkin Farm, Sorted Manual, Sorted Newest First, Sorted Oldest First, Sorted Title, Test Album"
)
@@ -4054,6 +4049,10 @@ def test_no_folder_1_15():
"AlbumInFolder",
"I have a deleted twin",
"Multi Keyword",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
]

View File

@@ -653,6 +653,10 @@ def test_subst_multi_folder_albums_1(photosdb):
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
"2019-10/11 Paris Clermont",
"Folder1/SubFolder2/AlbumInFolder",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
]
rendered, unknown = photo.render_template(template)
assert sorted(rendered) == sorted(expected)
@@ -669,6 +673,10 @@ def test_subst_multi_folder_albums_1_path_sep(photosdb):
"2018-10 - Sponsion, Museum, Frühstück, Römermuseum",
"2019-10/11 Paris Clermont",
"Folder1:SubFolder2:AlbumInFolder",
"Sorted Manual",
"Sorted Newest First",
"Sorted Oldest First",
"Sorted Title",
]
rendered, unknown = photo.render_template(template)
assert sorted(rendered) == sorted(expected)
@@ -685,6 +693,10 @@ def test_subst_multi_folder_albums_1_path_sep_lower(photosdb):
"2018-10 - sponsion, museum, frühstück, römermuseum",
"2019-10/11 paris clermont",
"folder1:subfolder2:albuminfolder",
"sorted manual",
"sorted newest first",
"sorted oldest first",
"sorted title",
]
rendered, unknown = photo.render_template(template)
assert sorted(rendered) == sorted(expected)