176 lines
5.5 KiB
Python
176 lines
5.5 KiB
Python
""" Example showing how to use a custom function for osxphotos {function} template
|
|
to export photos in a folder structure similar to Photos' own structure
|
|
|
|
Use: osxphotos export /path/to/export --directory "{function:/path/to/export_template.py::photos_folders}"
|
|
|
|
This will likely export multiple copies of each photo. If using APFS file system, this should be
|
|
a non-issue as osxphotos will use copy-on-write so each exported photo doesn't take up additional space
|
|
unless you edit the photo.
|
|
|
|
Thank-you @mkirkland4874 for the inspiration for this example!
|
|
|
|
This will produce output similar to this:
|
|
|
|
Library
|
|
- Photos
|
|
-- {created.year}
|
|
---- {created.mm}
|
|
------ {created.dd}
|
|
- Favorites
|
|
- Hidden
|
|
- Recently Deleted
|
|
- People
|
|
- Places
|
|
- Imports
|
|
Media Types
|
|
- Videos
|
|
- Selfies
|
|
- Portrait
|
|
- Panoramas
|
|
- Time-lapse
|
|
- Slow-mo
|
|
- Bursts
|
|
- Screenshots
|
|
My Albums
|
|
-- Album 1
|
|
-- Album 2
|
|
-- Folder 1
|
|
---- Album 3
|
|
Shared Albums
|
|
-- Shared Album 1
|
|
-- Shared Album 2
|
|
"""
|
|
|
|
from typing import List, Union
|
|
|
|
import osxphotos
|
|
from osxphotos._constants import _UNKNOWN_PERSON
|
|
from osxphotos.datetime_formatter import DateTimeFormatter
|
|
from osxphotos.path_utils import sanitize_dirname
|
|
from osxphotos.phototemplate import RenderOptions
|
|
|
|
|
|
def place_folder(photo: osxphotos.PhotoInfo) -> str:
|
|
"""Return places as folder in format Country/State/City/etc."""
|
|
if not photo.place:
|
|
return ""
|
|
|
|
places = []
|
|
if photo.place.names.country:
|
|
places.append(photo.place.names.country[0])
|
|
|
|
if photo.place.names.state_province:
|
|
places.append(photo.place.names.state_province[0])
|
|
|
|
if photo.place.names.sub_administrative_area:
|
|
places.append(photo.place.names.sub_administrative_area[0])
|
|
|
|
if photo.place.names.additional_city_info:
|
|
places.append(photo.place.names.additional_city_info[0])
|
|
|
|
if photo.place.names.area_of_interest:
|
|
places.append(photo.place.names.area_of_interest[0])
|
|
|
|
if places:
|
|
return "Library/Places/" + "/".join(sanitize_dirname(place) for place in places)
|
|
else:
|
|
return ""
|
|
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
rendered_date, _ = photo.render_template("{created.year}/{created.mm}/{created.dd}")
|
|
date_path = rendered_date[0]
|
|
|
|
def add_date_path(path):
|
|
"""add date path (year/mm/dd)"""
|
|
return f"{path}/{date_path}"
|
|
|
|
# Library
|
|
|
|
directories = []
|
|
if not photo.hidden and not photo.intrash and not photo.shared:
|
|
# set directories to [Library/Photos/year/mm/dd]
|
|
# render_template returns a tuple of [rendered value(s)], [unmatched]
|
|
# here, we can ignore the unmatched value, assigned to _, as we know template will match
|
|
directories, _ = photo.render_template(
|
|
"Library/Photos/{created.year}/{created.mm}/{created.dd}"
|
|
)
|
|
|
|
if photo.favorite:
|
|
directories.append(add_date_path("Library/Favorites"))
|
|
if photo.hidden:
|
|
directories.append(add_date_path("Library/Hidden"))
|
|
if photo.intrash:
|
|
directories.append(add_date_path("Library/Recently Deleted"))
|
|
|
|
directories.extend(
|
|
[
|
|
add_date_path(f"Library/People/{person}")
|
|
for person in photo.persons
|
|
if person != _UNKNOWN_PERSON
|
|
]
|
|
)
|
|
|
|
if photo.place:
|
|
directories.append(add_date_path(place_folder(photo)))
|
|
|
|
if photo.import_info:
|
|
dt = DateTimeFormatter(photo.import_info.creation_date)
|
|
directories.append(f"Library/Imports/{dt.year}/{dt.mm}/{dt.dd}")
|
|
|
|
# Media Types
|
|
|
|
if photo.ismovie:
|
|
directories.append(add_date_path("Media Types/Videos"))
|
|
if photo.selfie:
|
|
directories.append(add_date_path("Media Types/Selfies"))
|
|
if photo.live_photo:
|
|
directories.append(add_date_path("Media Types/Live Photos"))
|
|
if photo.portrait:
|
|
directories.append(add_date_path("Media Types/Portrait"))
|
|
if photo.panorama:
|
|
directories.append(add_date_path("Media Types/Panoramas"))
|
|
if photo.time_lapse:
|
|
directories.append(add_date_path("Media Types/Time-lapse"))
|
|
if photo.slow_mo:
|
|
directories.append(add_date_path("Media Types/Slo-mo"))
|
|
if photo.burst:
|
|
directories.append(add_date_path("Media Types/Bursts"))
|
|
if photo.screenshot:
|
|
directories.append(add_date_path("Media Types/Screenshots"))
|
|
|
|
# Albums
|
|
|
|
# render the folders and albums in folder/subfolder/album format
|
|
# the __NO_ALBUM__ is used as a sentinel to strip out photos not in an album
|
|
# use RenderOptions.dirname to force the rendered folder_album value to be sanitized as a valid path
|
|
# use RenderOptions.none_str to specify custom value for any photo that doesn't belong to an album so
|
|
# those can be filtered out; if not specified, none_str is "_"
|
|
folder_albums, _ = photo.render_template(
|
|
"{folder_album}", RenderOptions(dirname=True, none_str="__NO_ALBUM__")
|
|
)
|
|
|
|
root_directory = "Shared Albums/" if photo.shared else "My Albums/"
|
|
directories.extend(
|
|
[
|
|
root_directory + folder_album
|
|
for folder_album in folder_albums
|
|
if folder_album != "__NO_ALBUM__"
|
|
]
|
|
)
|
|
|
|
return directories
|