Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a05e7be14e | ||
|
|
e27c40c772 | ||
|
|
e752f3c7a7 | ||
|
|
6f4cab6721 | ||
|
|
2d899ef045 | ||
|
|
4f17c8fb23 | ||
|
|
173a0fce28 | ||
|
|
b04ea8174d | ||
|
|
e40ecc45ad | ||
|
|
277b1614b9 | ||
|
|
88099de688 | ||
|
|
7d81b94c16 | ||
|
|
d627cfc4fa | ||
|
|
bf208bbe4b | ||
|
|
79ba6f813f |
28
CHANGELOG.md
28
CHANGELOG.md
@@ -4,6 +4,34 @@ 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
|
||||
|
||||
- Upgraded osxmetadata to add new extended attributes [`7d81b94`](https://github.com/RhetTbull/osxphotos/commit/7d81b94c16623d11312aaf1b0c47fb580d01bc66)
|
||||
- Updated tutorial with --regex example [skip ci] [`bf208bb`](https://github.com/RhetTbull/osxphotos/commit/bf208bbe4b965a2d39fc1836335b7b65f402af30)
|
||||
- Update README.md [`d627cfc`](https://github.com/RhetTbull/osxphotos/commit/d627cfc4fa22497769babc3d686393c6043d1f37)
|
||||
|
||||
#### [v0.42.61](https://github.com/RhetTbull/osxphotos/compare/v0.42.60...v0.42.61)
|
||||
|
||||
> 7 July 2021
|
||||
|
||||
- Added --selected, closes #489 [`#489`](https://github.com/RhetTbull/osxphotos/issues/489)
|
||||
|
||||
#### [v0.42.60](https://github.com/RhetTbull/osxphotos/compare/v0.42.59...v0.42.60)
|
||||
|
||||
> 6 July 2021
|
||||
|
||||
62
README.md
62
README.md
@@ -3,6 +3,7 @@
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
[](https://github.com/RhetTbull/osxphotos/workflows/Tests/badge.svg)
|
||||

|
||||
[](https://pepy.tech/project/osxphotos)
|
||||
[](#contributors)
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
@@ -409,6 +410,12 @@ To export only photos contained in the album "Summer Vacation":
|
||||
|
||||
`osxphotos export /path/to/export --album "Summer Vacation"`
|
||||
|
||||
In Photos, it's possible to have multiple albums with the same name. In this case, osxphotos would export photos from all albums matching the value passed to `--album`. If you wanted to export only one of the albums and this album is in a folder, the `--regex` option (short for "regular expression"), which does pattern matching, could be used with the `{folder_album}` template to match the specific album. For example, if you had a "Summer Vacation" album inside the folder "2018" and also one with the same name inside the folder "2019", you could export just the album "2018/Summer Vacation" using this command:
|
||||
|
||||
`osxphotos export /path/to/export --regex "2018/Summer Vacation" "{folder_album}"`
|
||||
|
||||
This command matches the pattern "2018/Summer Vacation" against the full folder/album path for every photo.
|
||||
|
||||
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
|
||||
|
||||
`osxphotos export /path/to/export --portrait`
|
||||
@@ -1072,8 +1079,10 @@ Options:
|
||||
--xattr-template ATTRIBUTE TEMPLATE
|
||||
Set extended attribute ATTRIBUTE to TEMPLATE
|
||||
value. Valid attributes are: 'authors',
|
||||
'comment', 'copyright', 'description',
|
||||
'findercomment', 'headline', 'keywords'. For
|
||||
'comment', 'copyright', 'creator',
|
||||
'description', 'findercomment', 'headline',
|
||||
'keywords', 'participants', 'projects',
|
||||
'rating', 'subject', 'title', 'version'. For
|
||||
example, to set Finder comment to the photo's
|
||||
title and description: '--xattr-template
|
||||
findercomment "{title}; {descr}" See Extended
|
||||
@@ -1305,6 +1314,10 @@ comment A comment related to the file. This differs from the Finder
|
||||
copyright The copyright owner of the file contents. A string.
|
||||
(com.apple.metadata:kMDItemCopyright)
|
||||
|
||||
creator Application used to create the document content (for example
|
||||
“Word”, “Pages”, and so on). A string.
|
||||
(com.apple.metadata:kMDItemCreator)
|
||||
|
||||
description A description of the content of the resource. The description
|
||||
may include an abstract, table of contents, reference to a
|
||||
graphical representation of content or a free-text account of
|
||||
@@ -1322,6 +1335,29 @@ keywords Keywords associated with this file. For example, “Birthday”,
|
||||
and searchable in Spotlight using "tag:tag_name". A list of
|
||||
strings. (com.apple.metadata:kMDItemKeywords)
|
||||
|
||||
participants The list of people who are visible in an image or movie or
|
||||
written about in a document. A list of strings.
|
||||
(com.apple.metadata:kMDItemParticipants)
|
||||
|
||||
projects The list of projects that this file is part of. For example, if
|
||||
you were working on a movie all of the files could be marked as
|
||||
belonging to the project “My Movie”. A list of strings.
|
||||
(com.apple.metadata:kMDItemProjects)
|
||||
|
||||
rating User rating of this item. For example, the stars rating of an
|
||||
iTunes track. An integer.
|
||||
(com.apple.metadata:kMDItemStarRating)
|
||||
|
||||
subject Subject of the this item. A string.
|
||||
(com.apple.metadata:kMDItemSubject)
|
||||
|
||||
title The title of the file. For example, this could be the title of
|
||||
a document, the name of a song, or the subject of an email
|
||||
message. A string. (com.apple.metadata:kMDItemTitle)
|
||||
|
||||
version The version number of this file. A string.
|
||||
(com.apple.metadata:kMDItemVersion)
|
||||
|
||||
|
||||
For additional information on extended attributes see: https://developer.apple.c
|
||||
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
|
||||
@@ -1779,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.61'
|
||||
{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
|
||||
@@ -2515,7 +2551,7 @@ Returns the absolute path to the edited photo on disk as a string. If the photo
|
||||
**Note**: will also return None if the edited photo is missing on disk.
|
||||
|
||||
#### `path_derivatives`
|
||||
Returns list of paths to any derivative preview images associated with the photo. The list of returned paths is sorted in descieding order by size (the largest, presumably highest quality) preview image will be the first element in the returned list. These will be named something like this on Photos 5+:
|
||||
Returns list of paths to any derivative preview images associated with the photo. The list of returned paths is sorted in descending order by size (the largest, presumably highest quality) preview image will be the first element in the returned list. These will be named something like this on Photos 5+:
|
||||
|
||||
- `F19E06B8-A712-4B5C-907A-C007D37BDA16_1_101_o.jpeg`
|
||||
- `F19E06B8-A712-4B5C-907A-C007D37BDA16_1_102_o.jpeg`
|
||||
@@ -3002,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()
|
||||
@@ -3611,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.61'|
|
||||
|{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|
|
||||
|
||||
2
build.sh
2
build.sh
@@ -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)
|
||||
|
||||
129
examples/album_sort_order.py
Normal file
129
examples/album_sort_order.py
Normal file
@@ -0,0 +1,129 @@
|
||||
""" 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
|
||||
sort_order = 0 # change this to 1 if you want counting to start at 1
|
||||
for album_photo in album.photos:
|
||||
if album_photo.uuid == photo.uuid:
|
||||
# found the photo we're processing
|
||||
break
|
||||
sort_order += 1
|
||||
else:
|
||||
# didn't find the photo, so skip this file
|
||||
return None
|
||||
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
|
||||
):
|
||||
"""Call this with osxphotos export /path/to/export --post-function post_function.py::post_function
|
||||
This will get called immediately after the photo has been exported
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance for the photo that's just been exported
|
||||
results: ExportResults instance with information about the files associated with the exported photo
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
Will not be called if --dry-run flag is enabled
|
||||
Will be called immediately after export and before any --post-command commands are executed
|
||||
"""
|
||||
|
||||
# ExportResults has the following properties
|
||||
# fields with filenames contain the full path to the file
|
||||
# exported: list of all files exported
|
||||
# new: list of all new files exported (--update)
|
||||
# updated: list of all files updated (--update)
|
||||
# skipped: list of all files skipped (--update)
|
||||
# exif_updated: list of all files that were updated with --exiftool
|
||||
# touched: list of all files that had date updated with --touch-file
|
||||
# converted_to_jpeg: list of files converted to jpeg with --convert-to-jpeg
|
||||
# sidecar_json_written: list of all JSON sidecar files written
|
||||
# sidecar_json_skipped: list of all JSON sidecar files skipped (--update)
|
||||
# sidecar_exiftool_written: list of all exiftool sidecar files written
|
||||
# sidecar_exiftool_skipped: list of all exiftool sidecar files skipped (--update)
|
||||
# sidecar_xmp_written: list of all XMP sidecar files written
|
||||
# sidecar_xmp_skipped: list of all XMP sidecar files skipped (--update)
|
||||
# missing: list of all missing files
|
||||
# error: list tuples of (filename, error) for any errors generated during export
|
||||
# exiftool_warning: list of tuples of (filename, warning) for any warnings generated by exiftool with --exiftool
|
||||
# exiftool_error: list of tuples of (filename, error) for any errors generated by exiftool with --exiftool
|
||||
# xattr_written: list of files that had extended attributes written
|
||||
# xattr_skipped: list of files that where extended attributes were skipped (--update)
|
||||
# deleted_files: list of deleted files
|
||||
# deleted_directories: list of deleted directories
|
||||
# exported_album: list of tuples of (filename, album_name) for exported files added to album with --add-exported-to-album
|
||||
# skipped_album: list of tuples of (filename, album_name) for skipped files added to album with --add-skipped-to-album
|
||||
# missing_album: list of tuples of (filename, album_name) for missing files added to album with --add-missing-to-album
|
||||
|
||||
for filepath in results.exported:
|
||||
# do your processing here
|
||||
filepath = pathlib.Path(filepath)
|
||||
album_dir = filepath.parent.name
|
||||
if album_dir not in photo.albums:
|
||||
return
|
||||
|
||||
# get the first album that matches this name of which the photo is a member
|
||||
album_info = None
|
||||
for album in photo.album_info:
|
||||
if album.title == album_dir:
|
||||
album_info = album
|
||||
break
|
||||
else:
|
||||
# didn't find the album, so skip this file
|
||||
return
|
||||
|
||||
sort_order = _get_album_sort_order(album_info, photo)
|
||||
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))
|
||||
@@ -77,11 +77,12 @@ def place_folder(photo: osxphotos.PhotoInfo) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
def photos_folders(photo: osxphotos.PhotoInfo, **kwargs) -> Union[List, str]:
|
||||
def photos_folders(photo: osxphotos.PhotoInfo, options: osxphotos.phototemplate.RenderOptions, **kwargs) -> Union[List, str]:
|
||||
"""template function for use with --directory to export photos in a folder structure similar to Photos
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: RenderOptions instance
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns: list of directories for each photo
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from ._constants import AlbumSortOrder
|
||||
from ._version import __version__
|
||||
from .exiftool import ExifTool
|
||||
from .photoinfo import ExportResults, PhotoInfo
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -227,10 +228,17 @@ EXTENDED_ATTRIBUTE_NAMES = [
|
||||
"authors",
|
||||
"comment",
|
||||
"copyright",
|
||||
"creator",
|
||||
"description",
|
||||
"findercomment",
|
||||
"headline",
|
||||
"keywords",
|
||||
"participants",
|
||||
"projects",
|
||||
"rating",
|
||||
"subject",
|
||||
"title",
|
||||
"version",
|
||||
]
|
||||
EXTENDED_ATTRIBUTE_NAMES_QUOTED = [f"'{x}'" for x in EXTENDED_ATTRIBUTE_NAMES]
|
||||
|
||||
@@ -266,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
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.42.61"
|
||||
__version__ = "0.42.65"
|
||||
|
||||
@@ -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)
|
||||
|
||||
532
osxphotos/cli.py
532
osxphotos/cli.py
@@ -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)
|
||||
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -250,6 +250,7 @@ class RenderOptions:
|
||||
strip: if True, strips leading/trailing whitespace from rendered templates
|
||||
edited_version: set to True if you want {edited_version} to resolve to True (e.g. exporting edited version of photo)
|
||||
export_dir: set to the export directory if you want to evalute {export_dir} template
|
||||
dest_path: set to the destination path of the photo (for use by {function} template), only valid with --filename
|
||||
filepath: set to value for filepath of the exported photo if you want to evaluate {filepath} template
|
||||
quote: quote path templates for execution in the shell
|
||||
"""
|
||||
@@ -263,6 +264,7 @@ class RenderOptions:
|
||||
strip: bool = False
|
||||
edited_version: bool = False
|
||||
export_dir: Optional[str] = None
|
||||
dest_path: Optional[str] = None
|
||||
filepath: Optional[str] = None
|
||||
quote: bool = False
|
||||
|
||||
@@ -354,8 +356,10 @@ class PhotoTemplate:
|
||||
self.dirname = options.dirname
|
||||
self.strip = options.strip
|
||||
self.export_dir = options.export_dir
|
||||
self.dest_path = options.dest_path
|
||||
self.filepath = options.filepath
|
||||
self.quote = options.quote
|
||||
self.options = options
|
||||
|
||||
try:
|
||||
model = self.parser.parse(template)
|
||||
@@ -1182,7 +1186,7 @@ class PhotoTemplate:
|
||||
raise ValueError(f"'{filename}' does not appear to be a file")
|
||||
|
||||
template_func = load_function(filename_validated, funcname)
|
||||
values = template_func(self.photo)
|
||||
values = template_func(self.photo, options=self.options)
|
||||
|
||||
if not isinstance(values, (str, list)):
|
||||
raise TypeError(
|
||||
|
||||
@@ -238,6 +238,12 @@ To export only photos contained in the album "Summer Vacation":
|
||||
|
||||
`osxphotos export /path/to/export --album "Summer Vacation"`
|
||||
|
||||
In Photos, it's possible to have multiple albums with the same name. In this case, osxphotos would export photos from all albums matching the value passed to `--album`. If you wanted to export only one of the albums and this album is in a folder, the `--regex` option (short for "regular expression"), which does pattern matching, could be used with the `{folder_album}` template to match the specific album. For example, if you had a "Summer Vacation" album inside the folder "2018" and also one with the same name inside the folder "2019", you could export just the album "2018/Summer Vacation" using this command:
|
||||
|
||||
`osxphotos export /path/to/export --regex "2018/Summer Vacation" "{folder_album}"`
|
||||
|
||||
This command matches the pattern "2018/Summer Vacation" against the full folder/album path for every photo.
|
||||
|
||||
There are also a number of query options to export only certain types of photos. For example, to export only photos taken with iPhone "Portrait Mode":
|
||||
|
||||
`osxphotos export /path/to/export --portrait`
|
||||
|
||||
@@ -15,7 +15,7 @@ dataclasses==0.7;python_version<'3.7'
|
||||
wurlitzer==2.1.0
|
||||
photoscript==0.1.3
|
||||
toml==0.10.2
|
||||
osxmetadata==0.99.14
|
||||
osxmetadata==0.99.25
|
||||
textx==2.3.0
|
||||
rich==10.2.2
|
||||
bitmath==1.3.3.1
|
||||
|
||||
2
setup.py
2
setup.py
@@ -90,7 +90,7 @@ setup(
|
||||
"wurlitzer==2.1.0",
|
||||
"photoscript==0.1.3",
|
||||
"toml==0.10.2",
|
||||
"osxmetadata==0.99.14",
|
||||
"osxmetadata==0.99.25",
|
||||
"textx==2.3.0",
|
||||
"rich==10.2.2",
|
||||
"bitmath==1.3.3.1",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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>
|
||||
|
||||
@@ -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"}]
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}]
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user