Compare commits

..

14 Commits

Author SHA1 Message Date
Rhet Turnbull
94ac2bd04e Updated README.md [skip ci] 2021-06-30 22:43:56 -07:00
Rhet Turnbull
d1b1d20bcf Fixed --cleanup for empty export, #481 2021-06-30 22:41:03 -07:00
Rhet Turnbull
fb723fb8b7 Fixed raw+jpeg for Monterey 2021-06-29 18:22:48 -07:00
Rhet Turnbull
fc7c61b11b Merge branch 'master' of github.com:RhetTbull/osxphotos 2021-06-29 17:47:36 -07:00
Rhet Turnbull
a73db3a1bb Updated photokit code to work with raw+jpeg, #478 2021-06-29 17:47:21 -07:00
Rhet Turnbull
d2dcbaaec4 Updated photokit code to work with raw+jpeg 2021-06-29 17:46:40 -07:00
Rhet Turnbull
08147e91d9 Alpha support for Monterey/macOS 12 2021-06-29 13:32:36 -07:00
Rhet Turnbull
d034605784 Refactored UTI utils to get ready for Monterey 2021-06-29 09:31:22 -07:00
Rhet Turnbull
64fd852535 Updated README.md [skip ci] 2021-06-23 22:43:36 -07:00
Rhet Turnbull
3fbfc55e84 Fixed deprecation warning 2021-06-23 22:40:23 -07:00
Rhet Turnbull
49317582c4 Bug fix for template functions #477 2021-06-23 22:36:58 -07:00
Rhet Turnbull
5ea01df69b Bug fix 2021-06-21 06:34:56 -07:00
Rhet Turnbull
4a9f8a9ef5 Updated CHANGELOG.md [skip ci] 2021-06-20 18:19:21 -07:00
Rhet Turnbull
49adff1f3b Updated example [skip ci] 2021-06-20 18:11:41 -07:00
234 changed files with 3446 additions and 321 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.45](https://github.com/RhetTbull/osxphotos/compare/v0.42.44...v0.42.45)
> 20 June 2021
- Implemented --query-function, #430 [`07da803`](https://github.com/RhetTbull/osxphotos/commit/07da8031c63487eb42cb3e524f20971e6d2fc929)
- Added query function [skip ci] [`be363b9`](https://github.com/RhetTbull/osxphotos/commit/be363b9727d6fca6e747b0d952cd3252ddfe6e3b)
- Updated README.md [skip ci] [`377e165`](https://github.com/RhetTbull/osxphotos/commit/377e165be48b84c7678ca2f86fc2ffdcbcb93736)
#### [v0.42.44](https://github.com/RhetTbull/osxphotos/compare/v0.42.43...v0.42.44)
> 20 June 2021
- Added --location, --no-location, #474 [`870a59a`](https://github.com/RhetTbull/osxphotos/commit/870a59a2fa10766361b384216594af36d3605850)
#### [v0.42.43](https://github.com/RhetTbull/osxphotos/compare/v0.42.42...v0.42.43)
> 20 June 2021

196
README.md
View File

@@ -568,13 +568,13 @@ osxphotos is very flexible. If you merely want to backup your Photos library, t
Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST
Export photos from the Photos database. Export path DEST is required.
Optionally, query the Photos database using 1 or more search options; if more
than one option is provided, they are treated as "AND" (e.g. search for photos
matching all options). If no query options are provided, all photos will be
exported. By default, all versions of all photos will be exported including
edited versions, live photo movies, burst photos, and associated raw images.
See --skip-edited, --skip-live, --skip-bursts, and --skip-raw options to
modify this behavior.
Optionally, query the Photos database using 1 or more search options; if
more than one option is provided, they are treated as "AND" (e.g. search for
photos matching all options). If no query options are provided, all photos
will be exported. By default, all versions of all photos will be exported
including edited versions, live photo movies, burst photos, and associated
raw images. See --skip-edited, --skip-live, --skip-bursts, and --skip-raw
options to modify this behavior.
Options:
--db <Photos database path> Specify Photos database path. Path to Photos
@@ -585,49 +585,63 @@ Options:
use in the following order: 1. last opened
library, 2. system library, 3.
~/Pictures/Photos Library.photoslibrary
-V, --verbose Print verbose output.
--keyword KEYWORD Search for photos with keyword KEYWORD. If
more than one keyword, treated as "OR", e.g.
find photos matching any keyword
--person PERSON Search for photos with person PERSON. If more
than one person, treated as "OR", e.g. find
photos matching any person
--album ALBUM Search for photos in album ALBUM. If more than
one album, treated as "OR", e.g. find photos
matching any album
--folder FOLDER Search for photos in an album in folder
FOLDER. If more than one folder, treated as
"OR", e.g. find photos in any FOLDER. Only
searches top level folders (e.g. does not look
at subfolders)
--name FILENAME Search for photos with filename matching
FILENAME. If more than one --name options is
specified, they are treated as "OR", e.g. find
photos matching any FILENAME.
--uuid UUID Search for photos with UUID(s).
--uuid-from-file FILE Search for photos with UUID(s) loaded from
FILE. Format is a single UUID per line. Lines
preceded with # are ignored.
--title TITLE Search for TITLE in title of photo.
--no-title Search for photos with no title.
--description DESC Search for DESC in description of photo.
--no-description Search for photos with no description.
--place PLACE Search for PLACE in photo's reverse
geolocation info
--no-place Search for photos with no associated place
name info (no reverse geolocation info)
--location Search for photos with associated location
info (e.g. GPS coordinates)
--no-location Search for photos with no associated location
info (e.g. no GPS coordinates)
--label LABEL Search for photos with image classification
label LABEL (Photos 5 only). If more than one
label, treated as "OR", e.g. find photos
matching any label
--uti UTI Search for photos whose uniform type
identifier (UTI) matches UTI
-i, --ignore-case Case insensitive search for title,
description, place, keyword, person, or album.
--edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite.
@@ -636,51 +650,67 @@ Options:
--not-hidden Search for photos not marked hidden.
--shared Search for photos in shared iCloud album
(Photos 5 only).
--not-shared Search for photos not in shared iCloud album
(Photos 5 only).
--burst Search for photos that were taken in a burst.
--not-burst Search for photos that are not part of a
burst.
--live Search for Apple live photos
--not-live Search for photos that are not Apple live
photos.
--portrait Search for Apple portrait mode photos.
--not-portrait Search for photos that are not Apple portrait
mode photos.
--screenshot Search for screenshot photos.
--not-screenshot Search for photos that are not screenshot
photos.
--slow-mo Search for slow motion videos.
--not-slow-mo Search for photos that are not slow motion
videos.
--time-lapse Search for time lapse videos.
--not-time-lapse Search for photos that are not time lapse
videos.
--hdr Search for high dynamic range (HDR) photos.
--not-hdr Search for photos that are not HDR photos.
--selfie Search for selfies (photos taken with front-
facing cameras).
--not-selfie Search for photos that are not selfies.
--panorama Search for panorama photos.
--not-panorama Search for photos that are not panoramas.
--has-raw Search for photos with both a jpeg and raw
version
--only-movies Search only for movies (default searches both
images and movies).
--only-photos Search only for photos/images (default
searches both images and movies).
--from-date DATETIME Search by item start date, e.g.
2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601 with/without timezone).
--to-date DATETIME Search by item end date, e.g.
2000-01-12T12:00:00,
2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO
8601 with/without timezone).
--from-time TIME Search by item start time of day, e.g. 12:00,
or 12:00:00.
--to-time TIME Search by item end time of day, e.g. 12:00 or
12:00:00.
--has-comment Search for photos that have comments.
--no-comment Search for photos with no comments.
--has-likes Search for photos that have likes.
@@ -688,8 +718,10 @@ Options:
--is-reference Search for photos that were imported as
referenced files (not copied into Photos
library).
--in-album Search for photos that are in one or more
albums.
--not-in-album Search for photos that are not in any albums.
--duplicate Search for photos with possible duplicates.
osxphotos will compare signatures of photos,
@@ -699,6 +731,7 @@ Options:
for-byte nor compare hashes but should find
photos imported multiple times or duplicated
within Photos.
--min-size SIZE Search for photos with size >= SIZE bytes. The
size evaluated is the photo's original size
(when imported to Photos). Size may be
@@ -706,6 +739,7 @@ Options:
units. For example, the following are all
valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'.
--max-size SIZE Search for photos with size <= SIZE bytes. The
size evaluated is the photo's original size
(when imported to Photos). Size may be
@@ -713,12 +747,14 @@ Options:
units. For example, the following are all
valid and equivalent sizes: '1048576'
'1.048576MB', '1 MiB'.
--regex REGEX TEMPLATE Search for photos where TEMPLATE matches
regular expression REGEX. For example, to find
photos in an album that begins with 'Beach': '
--regex "^Beach" "{album}"'. You may specify
more than one regular expression match by
repeating '--regex' with different arguments.
--query-eval CRITERIA Evaluate CRITERIA to filter photos. CRITERIA
will be evaluated in context of the following
python list comprehension: `photos = [photo
@@ -733,6 +769,7 @@ Options:
https://rhettbull.github.io/osxphotos/ for
additional documentation on the PhotoInfo
class.
--query-function filename.py::function
Run function to filter photos. Use this in
format: --query-function filename.py::function
@@ -749,14 +786,19 @@ Options:
evaluated. See https://github.com/RhetTbull/os
xphotos/blob/master/examples/query_function.py
for example of how to use this option.
--missing Export only photos missing from the Photos
library; must be used with --download-missing.
--deleted Include photos from the 'Recently Deleted'
folder.
--deleted-only Include only photos from the 'Recently
Deleted' folder.
--update Only export new or updated files. See notes
below on export and --update.
--ignore-signature When used with '--update', ignores file
signature when updating files. This is useful
if you have processed or edited exported
@@ -775,12 +817,15 @@ Options:
not; 3) if a sidecar does not exist for the
photo, a sidecar will be written whether or
not the photo file was written or updated.
--only-new If used with --update, ignores any previously
exported files, even if missing from the
export folder and only exports new files that
haven't previously been exported.
--dry-run Dry run (test) the export but don't actually
export any files; most useful with --verbose.
--export-as-hardlink Hardlink files instead of copying them. Cannot
be used with --exiftool which creates copies
of the files with embedded EXIF data. Note: on
@@ -788,38 +833,49 @@ Options:
giving many of the same advantages as
hardlinks without having to use --export-as-
hardlink.
--touch-file Sets the file's modification time to match
photo date.
--overwrite Overwrite existing files. Default behavior is
to add (1), (2), etc to filename if file
already exists. Use this with caution as it
may create name collisions on export. (e.g. if
two files happen to have the same name)
--retry RETRY Automatically retry export up to RETRY times
if an error occurs during export. This may be
useful with network drives that experience
intermittent errors.
--export-by-date Automatically create output folders to
organize photos by date created (e.g.
DEST/2019/12/20/photoname.jpg).
--skip-edited Do not export edited version of photo if an
edited version exists.
--skip-original-if-edited Do not export original if there is an edited
version (exports only the edited version).
--skip-bursts Do not export all associated burst images in
the library if a photo is a burst photo.
--skip-live Do not export the associated live video
component of a live photo.
--skip-raw Do not export associated RAW image of a
RAW+JPEG pair. Note: this does not skip RAW
photos if the RAW photo does not have an
associated JPEG image (e.g. the RAW file was
imported to Photos without a JPEG preview).
--current-name Use photo's current filename instead of
original filename for export. Note: Starting
with Photos 5, all photos are renamed upon
import. By default, photos are exported with
the the original name they had before import.
--convert-to-jpeg Convert all non-JPEG images (e.g. RAW, HEIC,
PNG, etc) to JPEG upon export. Note: does not
convert the RAW component of a RAW+JPEG pair
@@ -829,10 +885,12 @@ Options:
also --jpeg-quality and --jpeg-ext. Only works
if your Mac has a GPU (thus may not work on
virtual machines).
--jpeg-quality FLOAT RANGE Value in range 0.0 to 1.0 to use with
--convert-to-jpeg. A value of 1.0 specifies
best quality, a value of 0.0 specifies maximum
compression. Defaults to 1.0 [0.0<=x<=1.0]
compression. Defaults to 1.0
--download-missing Attempt to download missing photos from
iCloud. The current implementation uses
Applescript to interact with Photos to export
@@ -845,6 +903,7 @@ Options:
export all burst images; only the primary
photo will be exported--associated burst
images will be skipped.
--sidecar FORMAT Create sidecar for each photo exported; valid
FORMAT values: xmp, json, exiftool; --sidecar
xmp: create XMP sidecar used by Digikam, Adobe
@@ -871,6 +930,7 @@ Options:
tags exported in the JSON and exiftool
sidecar, see '--exiftool'. See also '--ignore-
signature'.
--sidecar-drop-ext Drop the photo's extension when naming sidecar
files. By default, sidecar files are named in
format 'photo_filename.photo_ext.sidecar_ext',
@@ -882,6 +942,7 @@ Options:
of different types but the same name in the
output directory, e.g. 'IMG_1234.JPG' and
'IMG_1234.MOV'.
--exiftool Use exiftool to write metadata directly to
exported photos. To use this option, exiftool
must be installed and in the path. exiftool
@@ -903,8 +964,10 @@ Options:
QuickTime:ModifyDate (see also --ignore-date-
modified); QuickTime:GPSCoordinates;
UserData:GPSCoordinates.
--exiftool-path EXIFTOOL_PATH Optionally specify path to exiftool; if not
provided, will look for exiftool in $PATH.
--exiftool-option OPTION Optional flag/option to pass to exiftool when
using --exiftool. For example, --exiftool-
option '-m' to ignore minor warnings. Specify
@@ -914,21 +977,27 @@ Options:
full list of options. More than one option may
be specified by repeating the option, e.g.
--exiftool-option '-m' --exiftool-option '-F'.
--exiftool-merge-keywords Merge any keywords found in the original file
with keywords used for '--exiftool' and '--
sidecar'.
--exiftool-merge-persons Merge any persons found in the original file
with persons used for '--exiftool' and '--
sidecar'.
--ignore-date-modified If used with --exiftool or --sidecar, will
ignore the photo modification date and set
EXIF:ModifyDate to EXIF:DateTimeOriginal; this
is consistent with how Photos handles the
EXIF:ModifyDate tag.
--person-keyword Use person in image as keyword/tag when
exporting metadata.
--album-keyword Use album name as keyword/tag when exporting
metadata.
--keyword-template TEMPLATE For use with --exiftool, --sidecar; specify a
template string to use as keyword in the form
'{name,DEFAULT}' This is the same format as
@@ -941,6 +1010,7 @@ Options:
"{folder_album}" --keyword-template
"{created.year}". See '--replace-keywords' and
Templating System below.
--replace-keywords Replace keywords with any values specified
with --keyword-template. By default,
--keyword-template will add keywords to any
@@ -949,6 +1019,7 @@ Options:
from --keyword-template will replace any
existing keywords instead of adding additional
keywords.
--description-template TEMPLATE
For use with --exiftool, --sidecar; specify a
template string to use as description in the
@@ -959,6 +1030,7 @@ Options:
--description-template "{descr} exported with
osxphotos on {today.date}" See Templating
System below.
--finder-tag-template TEMPLATE Set MacOS Finder tags to TEMPLATE. These tags
can be searched in the Finder or Spotlight
with 'tag:tagname' format. For example, '--
@@ -967,11 +1039,13 @@ Options:
TEMPLATE values by using '--finder-tag-
template' multiple times. See also '--finder-
tag-keywords and Extended Attributes below.'.
--finder-tag-keywords Set MacOS Finder tags to keywords; any
keywords specified via '--keyword-template', '
--person-keyword', etc. will also be used as
Finder tags. See also '--finder-tag-template
and Extended Attributes below.'.
--xattr-template ATTRIBUTE TEMPLATE
Set extended attribute ATTRIBUTE to TEMPLATE
value. Valid attributes are: 'authors',
@@ -982,16 +1056,19 @@ Options:
findercomment "{title}; {descr}" See Extended
Attributes below for additional details on
this option.
--directory DIRECTORY Optional template for specifying name of
output directory in the form '{name,DEFAULT}'.
See below for additional details on templating
system.
--filename FILENAME Optional template for specifying name of
output file in the form '{name,DEFAULT}'. File
extension will be added automatically--do not
include an extension in the FILENAME template.
See below for additional details on templating
system.
--jpeg-ext EXTENSION Specify file extension for JPEG files. Photos
uses .jpeg for edited images but many images
are imported with .jpg or .JPG which can
@@ -1001,12 +1078,14 @@ Options:
exported JPEG images. Valid values are jpeg,
jpg, JPEG, JPG; e.g. '--jpeg-ext jpg' to use
'.jpg' for all JPEGs.
--strip Optionally strip leading and trailing
whitespace from any rendered templates. For
example, if --filename template is "{title,}
{original_name}" and image has no title,
resulting file would have a leading space but
if used with --strip, this will be removed.
--edited-suffix SUFFIX Optional suffix template for naming edited
photos. Default name for edited photos is in
form 'photoname_edited.ext'. For example, with
@@ -1016,6 +1095,7 @@ Options:
suffix is '_edited'. Multi-value templates
(see Templating System) are not permitted with
--edited-suffix.
--original-suffix SUFFIX Optional suffix template for naming original
photos. Default name for original photos is
in form 'filename.ext'. For example, with '--
@@ -1024,9 +1104,11 @@ Options:
default suffix is '' (no suffix). Multi-value
templates (see Templating System) are not
permitted with --original-suffix.
--use-photos-export Force the use of AppleScript or PhotoKit to
export even if not missing (see also '--
download-missing' and '--use-photokit').
--use-photokit Use with '--download-missing' or '--use-
photos-export' to use direct Photos interface
instead of AppleScript to export. Highly
@@ -1034,9 +1116,11 @@ Options:
iTerm2 (use with Terminal.app). This is faster
and more reliable than the default AppleScript
interface.
--report <path to export report>
Write a CSV formatted report of all files that
were exported.
--cleanup Cleanup export directory by deleting any files
which were not included in this export set.
For example, photos which had previously been
@@ -1048,6 +1132,7 @@ Options:
you intend before using --cleanup. Use --dry-
run with --cleanup first if you're not
certain.
--add-exported-to-album ALBUM Add all exported photos to album ALBUM in
Photos. Album ALBUM will be created if it
doesn't exist. All exported photos will be
@@ -1057,6 +1142,7 @@ Options:
feature is currently experimental. I don't
know how well it will work on large export
sets.
--add-skipped-to-album ALBUM Add all skipped photos to album ALBUM in
Photos. Album ALBUM will be created if it
doesn't exist. All skipped photos will be
@@ -1066,6 +1152,7 @@ Options:
feature is currently experimental. I don't
know how well it will work on large export
sets.
--add-missing-to-album ALBUM Add all missing photos to album ALBUM in
Photos. Album ALBUM will be created if it
doesn't exist. All missing photos will be
@@ -1075,6 +1162,7 @@ Options:
feature is currently experimental. I don't
know how well it will work on large export
sets.
--post-command CATEGORY COMMAND
Run COMMAND on exported files of category
CATEGORY. CATEGORY can be one of: exported,
@@ -1093,6 +1181,7 @@ Options:
command by repeating the '--post-command'
option with different arguments. See Post
Command below.
--post-function filename.py::function
Run function on exported files. Use this in
format: --post-function filename.py::function
@@ -1105,6 +1194,7 @@ Options:
You can run more than one function by
repeating the '--post-function' option with
different arguments. See Post Function below.
--exportdb EXPORTDB_FILE Specify alternate name for database file which
stores state information for export and
--update. If --exportdb is not specified,
@@ -1113,6 +1203,7 @@ Options:
directory. Must be specified as filename
only, not a path, as export database will be
saved in export directory.
--load-config <config file path>
Load options from file as written with --save-
config. This allows you to save a complex
@@ -1124,9 +1215,11 @@ Options:
line options are used in conjunction with
--load-config, they will override the
corresponding values in the config file.
--save-config <config file path>
Save options to file for use with --load-
config. File format is TOML.
--help Show this message and exit.
** Export **
@@ -1181,25 +1274,32 @@ The following attributes may be used with '--xattr-template':
authors The author, or authors, of the contents of the file. A list of
strings. (com.apple.metadata:kMDItemAuthors)
comment A comment related to the file. This differs from the Finder
comment, kMDItemFinderComment. A string.
(com.apple.metadata:kMDItemComment)
copyright The copyright owner of the file contents. A string.
(com.apple.metadata:kMDItemCopyright)
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
the content. A string. (com.apple.metadata:kMDItemDescription)
findercomment Finder comments for this file. A string.
(com.apple.metadata:kMDItemFinderComment)
headline A publishable entry providing a synopsis of the contents of the
file. A string. (com.apple.metadata:kMDItemHeadline)
keywords Keywords associated with this file. For example, “Birthday”,
“Important”, etc. This differs from Finder tags
(_kMDItemUserTags) which are keywords/tags shown in the Finder
and searchable in Spotlight using "tag:tag_name". A list of
strings. (com.apple.metadata:kMDItemKeywords)
For additional information on extended attributes see: https://developer.apple.c
om/documentation/coreservices/file_metadata/mditem/common_metadata_attribute_key
s
@@ -1425,6 +1525,7 @@ Substitution Description
{name} Current filename of the photo
{original_name} Photo's original filename when imported to
Photos
{title} Title of the photo
{descr} Description of the photo
{media_type} Special media type resolved in this
@@ -1434,35 +1535,48 @@ Substitution Description
'video' if no special type. Customize one or
more media types using format: '{media_type,vi
deo=vidéo;time_lapse=vidéo_accélérée}'
{photo_or_video} 'photo' or 'video' depending on what type the
image is. To customize, use default value as
in '{photo_or_video,photo=fotos;video=videos}'
{hdr} Photo is HDR?; True/False value, use in format
'{hdr?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited} True if photo has been edited (has
adjustments), otherwise False; use in format
'{edited?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{edited_version} True if template is being rendered for the
edited version of a photo, otherwise False.
{favorite} Photo has been marked as favorite?; True/False
value, use in format
'{favorite?VALUE_IF_TRUE,VALUE_IF_FALSE}'
{created.date} Photo's creation date in ISO format, e.g.
'2020-03-22'
{created.year} 4-digit year of photo creation time
{created.yy} 2-digit year of photo creation time
{created.mm} 2-digit month of the photo creation time (zero
padded)
{created.month} Month name in user's locale of the photo
creation time
{created.mon} Month abbreviation in the user's locale of the
photo creation time
{created.dd} 2-digit day of the month (zero padded) of
photo creation time
{created.dow} Day of week in user's locale of the photo
creation time
{created.doy} 3-digit day of year (e.g Julian day) of photo
creation time, starting from 1 (zero padded)
{created.hour} 2-digit hour of the photo creation time
{created.min} 2-digit minute of the photo creation time
{created.sec} 2-digit second of the photo creation time
@@ -1475,38 +1589,51 @@ Substitution Description
no template will return null value. See
https://strftime.org/ for help on strftime
templates.
{modified.date} Photo's modification date in ISO format, e.g.
'2020-03-22'; uses creation date if photo is
not modified
{modified.year} 4-digit year of photo modification time; uses
creation date if photo is not modified
{modified.yy} 2-digit year of photo modification time; uses
creation date if photo is not modified
{modified.mm} 2-digit month of the photo modification time
(zero padded); uses creation date if photo is
not modified
{modified.month} Month name in user's locale of the photo
modification time; uses creation date if photo
is not modified
{modified.mon} Month abbreviation in the user's locale of the
photo modification time; uses creation date if
photo is not modified
{modified.dd} 2-digit day of the month (zero padded) of the
photo modification time; uses creation date if
photo is not modified
{modified.dow} Day of week in user's locale of the photo
modification time; uses creation date if photo
is not modified
{modified.doy} 3-digit day of year (e.g Julian day) of photo
modification time, starting from 1 (zero
padded); uses creation date if photo is not
modified
{modified.hour} 2-digit hour of the photo modification time;
uses creation date if photo is not modified
{modified.min} 2-digit minute of the photo modification time;
uses creation date if photo is not modified
{modified.sec} 2-digit second of the photo modification time;
uses creation date if photo is not modified
{modified.strftime} Apply strftime template to file modification
date/time. Should be used in form
{modified.strftime,TEMPLATE} where TEMPLATE is
@@ -1517,21 +1644,28 @@ Substitution Description
creation date if photo is not modified. See
https://strftime.org/ for help on strftime
templates.
{today.date} Current date in iso format, e.g. '2020-03-22'
{today.year} 4-digit year of current date
{today.yy} 2-digit year of current date
{today.mm} 2-digit month of the current date (zero
padded)
{today.month} Month name in user's locale of the current
date
{today.mon} Month abbreviation in the user's locale of the
current date
{today.dd} 2-digit day of the month (zero padded) of
current date
{today.dow} Day of week in user's locale of the current
date
{today.doy} 3-digit day of year (e.g Julian day) of
current date, starting from 1 (zero padded)
{today.hour} 2-digit hour of the current date
{today.min} 2-digit minute of the current date
{today.sec} 2-digit second of the current date
@@ -1544,51 +1678,70 @@ Substitution Description
no template will return null value. See
https://strftime.org/ for help on strftime
templates.
{place.name} Place name from the photo's reverse
geolocation data, as displayed in Photos
{place.country_code} The ISO country code from the photo's reverse
geolocation data
{place.name.country} Country name from the photo's reverse
geolocation data
{place.name.state_province} State or province name from the photo's
reverse geolocation data
{place.name.city} City or locality name from the photo's reverse
geolocation data
{place.name.area_of_interest} Area of interest name (e.g. landmark or public
place) from the photo's reverse geolocation
data
{place.address} Postal address from the photo's reverse
geolocation data, e.g. '2007 18th St NW,
Washington, DC 20009, United States'
{place.address.street} Street part of the postal address, e.g. '2007
18th St NW'
{place.address.city} City part of the postal address, e.g.
'Washington'
{place.address.state_province} State/province part of the postal address,
e.g. 'DC'
{place.address.postal_code} Postal code part of the postal address, e.g.
'20009'
{place.address.country} Country name of the postal address, e.g.
'United States'
{place.address.country_code} ISO country code of the postal address, e.g.
'US'
{searchinfo.season} Season of the year associated with a photo,
e.g. 'Summer'; (Photos 5+ only, applied
automatically by Photos' image categorization
algorithms).
{exif.camera_make} Camera make from original photo's EXIF
information as imported by Photos, e.g.
'Apple'
{exif.camera_model} Camera model from original photo's EXIF
information as imported by Photos, e.g.
'iPhone 6s'
{exif.lens_model} Lens model from original photo's EXIF
information as imported by Photos, e.g.
'iPhone 6s back camera 4.15mm f/2.2'
{uuid} Photo's internal universally unique identifier
(UUID) for the photo, a 36-character string
unique to the photo, e.g.
'128FB4C6-0B16-4E7D-9108-FB2E90DA1546'
{comma} A comma: ','
{semicolon} A semicolon: ';'
{questionmark} A question mark: '?'
@@ -1603,7 +1756,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.45'
{osxphotos_version} The osxphotos version, e.g. '0.42.51'
{osxphotos_cmd_line} The full command line used to run osxphotos
The following substitutions may result in multiple values. Thus if specified for
@@ -1618,6 +1771,7 @@ Substitution Description
{folder_album} Folder path + album photo is contained in. e.g.
'Folder/Subfolder/Album' or just 'Album' if no
enclosing folder
{keyword} Keyword(s) assigned to photo
{person} Person(s) / face(s) in a photo
{label} Image categorization label associated with a photo
@@ -1626,9 +1780,11 @@ Substitution Description
categorize images. These are not the same as
{keyword} which refers to the user-defined
keywords/tags applied in Photos.
{label_normalized} All lower case version of 'label' (Photos 5+ only)
{comment} Comment(s) on shared Photos; format is 'Person name:
comment text' (Photos 5+ only)
{exiftool} Format: '{exiftool:GROUP:TAGNAME}'; use exiftool
(https://exiftool.org) to extract metadata, in form
GROUP:TAGNAME, from image. E.g.
@@ -1638,19 +1794,24 @@ Substitution Description
names. You must specify group (e.g. EXIF, IPTC, etc)
as used in `exiftool -G`. exiftool must be installed
in the path to use this template.
{searchinfo.holiday} Holiday names associated with a photo, e.g.
'Christmas Day'; (Photos 5+ only, applied
automatically by Photos' image categorization
algorithms).
{searchinfo.activity} Activities associated with a photo, e.g. 'Sporting
Event'; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms).
{searchinfo.venue} Venues associated with a photo, e.g. name of
restaurant; (Photos 5+ only, applied automatically by
Photos' image categorization algorithms).
{searchinfo.venue_type} Venue types associated with a photo, e.g.
'Restaurant'; (Photos 5+ only, applied automatically
by Photos' image categorization algorithms).
{photo} Provides direct access to the PhotoInfo object for
the photo. Must be used in format '{photo.property}'
where 'property' represents a PhotoInfo property. For
@@ -1662,10 +1823,12 @@ Substitution Description
underlying PhotoInfo class. See
https://rhettbull.github.io/osxphotos/ for additional
documentation on the PhotoInfo class.
{shell_quote} Use in form '{shell_quote,TEMPLATE}'; quotes the
rendered TEMPLATE value(s) for safe usage in the
shell, e.g. My file.jpeg => 'My file.jpeg'; only adds
quotes if needed.
{function} Execute a python function from an external file and
use return value as template substitution. Use in
format: {function:file.py::function_name} where
@@ -1676,6 +1839,7 @@ Substitution Description
/blob/master/examples/template_function.py for an
example of how to implement a template function.
The following substitutions are file or directory paths. You can access various
parts of the path using the following modifiers:
@@ -1710,29 +1874,41 @@ exported All exported files
new When used with '--update', all newly exported files
updated When used with '--update', all files which were
previously exported but updated this time
skipped When used with '--update', all files which were
skipped (because they were previously exported and
didn't change)
missing All files which were not exported because they were
missing from the Photos library
exif_updated When used with '--exiftool', all files on which
exiftool updated the metadata
touched When used with '--touch-file', all files where the
date was touched
converted_to_jpeg When used with '--convert-to-jpeg', all files which
were converted to jpeg
sidecar_json_written When used with '--sidecar json', all JSON sidecar
files which were written
sidecar_json_skipped When used with '--sidecar json' and '--update', all
JSON sidecar files which were skipped
sidecar_exiftool_written When used with '--sidecar exiftool', all exiftool
sidecar files which were written
sidecar_exiftool_skipped When used with '--sidecar exiftool' and '--update,
all exiftool sidecar files which were skipped
sidecar_xmp_written When used with '--sidecar xmp', all XMP sidecar
files which were written
sidecar_xmp_skipped When used with '--sidecar xmp' and '--update', all
XMP sidecar files which were skipped
error All files which produced an error during export
In addition to all normal template fields, the template fields '{filepath}' and
@@ -3402,7 +3578,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.45'|
|{osxphotos_version}|The osxphotos version, e.g. '0.42.51'|
|{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

@@ -5,7 +5,7 @@ from typing import List
from osxphotos import PhotoInfo
# call this with --query-function:examples/query_function.py::best_selfies
# call this with --query-function examples/query_function.py::best_selfies
def best_selfies(photos: List[PhotoInfo]) -> List[PhotoInfo]:
"""your query function should take a list of PhotoInfo objects and return a list of PhotoInfo objects (or empty list)"""
# this example finds your best selfie for every year

View File

@@ -34,11 +34,12 @@ _PHOTOS_3_VERSION = "3301"
# versions 5.0 and later have a different database structure
_PHOTOS_4_VERSION = "4025" # latest Mojove version on 10.14.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.6
_PHOTOS_5_VERSION = "6000" # seems to be current on 10.15.1 through 10.15.7 (also Big Sur and Monterey which switch to model version)
# Ranges for model version by Photos version
_PHOTOS_5_MODEL_VERSION = [13000, 13999]
_PHOTOS_6_MODEL_VERSION = [14000, 14999]
_PHOTOS_7_MODEL_VERSION = [15000, 15999] # Monterey developer preview is 15134
# some table names differ between Photos 5 and Photos 6
_DB_TABLE_NAMES = {
@@ -49,6 +50,10 @@ _DB_TABLE_NAMES = {
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_34ASSETS",
"IMPORT_FOK": "ZGENERICASSET.Z_FOK_IMPORTSESSION",
"DEPTH_STATE": "ZGENERICASSET.ZDEPTHSTATES",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
},
6: {
"ASSET": "ZASSET",
@@ -57,6 +62,22 @@ _DB_TABLE_NAMES = {
"ALBUM_SORT_ORDER": "Z_26ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER",
"ASSET_ALBUM_JOIN": "Z_26ASSETS.Z_26ALBUMS",
"ASSET_ALBUM_TABLE": "Z_26ASSETS",
"HDR_TYPE": "ZCUSTOMRENDEREDVALUE",
},
7: {
"ASSET": "ZASSET",
"KEYWORD_JOIN": "Z_1KEYWORDS.Z_38KEYWORDS",
"ALBUM_JOIN": "Z_27ASSETS.Z_3ASSETS",
"ALBUM_SORT_ORDER": "Z_27ASSETS.Z_FOK_3ASSETS",
"IMPORT_FOK": "null",
"DEPTH_STATE": "ZASSET.ZDEPTHTYPE",
"UTI_ORIGINAL": "ZINTERNALRESOURCE.ZCOMPACTUTI",
"ASSET_ALBUM_JOIN": "Z_27ASSETS.Z_27ALBUMS",
"ASSET_ALBUM_TABLE": "Z_27ASSETS",
"HDR_TYPE": "ZHDRTYPE",
},
}

View File

@@ -1,3 +1,3 @@
""" version info """
__version__ = "0.42.45"
__version__ = "0.42.51"

View File

@@ -57,7 +57,8 @@ from .photokit import check_photokit_authorization, request_photokit_authorizati
from .photosalbum import PhotosAlbum
from .phototemplate import PhotoTemplate, RenderOptions
from .queryoptions import QueryOptions
from .utils import get_preferred_uti_extension, load_function
from .uti import get_preferred_uti_extension
from .utils import expand_and_validate_filepath, load_function
# global variable to control verbose output
# set via --verbose/-V
@@ -172,13 +173,14 @@ class FunctionCall(click.ParamType):
filename, funcname = value.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
self.fail(f"'{filename}' does not appear to be a file")
try:
function = load_function(filename, funcname)
function = load_function(filename_validated, funcname)
except Exception as e:
self.fail(f"Could not load function {funcname} from {filename}")
self.fail(f"Could not load function {funcname} from {filename_validated}")
return (function, value)
@@ -1645,6 +1647,9 @@ def export(
previous_uuids = {uuid: 1 for uuid in export_db.get_previous_uuids()}
photos = [p for p in photos if p.uuid not in previous_uuids]
# store results of export
results = ExportResults()
if photos:
num_photos = len(photos)
# TODO: photos or photo appears several times, pull into a separate function
@@ -1656,8 +1661,6 @@ def export(
# because the original code used --original-name as an option
original_name = not current_name
results = ExportResults()
# set up for --add-export-to-album if needed
album_export = (
PhotosAlbum(add_exported_to_album, verbose=verbose_)
@@ -1832,41 +1835,6 @@ def export(
if fp is not None:
fp.close()
if cleanup:
all_files = (
results.exported
+ results.skipped
+ results.exif_updated
+ results.touched
+ results.converted_to_jpeg
+ results.sidecar_json_written
+ results.sidecar_json_skipped
+ results.sidecar_exiftool_written
+ results.sidecar_exiftool_skipped
+ results.sidecar_xmp_written
+ results.sidecar_xmp_skipped
# include missing so a file that was already in export directory
# but was missing on --update doesn't get deleted
# (better to have old version than none)
+ results.missing
# include files that have error in case they exist from previous export
+ [r[0] for r in results.error]
+ [str(pathlib.Path(export_db_path).resolve())]
)
click.echo(f"Cleaning up {dest}")
cleaned_files, cleaned_dirs = cleanup_files(dest, all_files, fileutil)
file_str = "files" if len(cleaned_files) != 1 else "file"
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
click.echo(
f"Deleted: {len(cleaned_files)} {file_str}, {len(cleaned_dirs)} {dir_str}"
)
results.deleted_files = cleaned_files
results.deleted_directories = cleaned_dirs
if report:
verbose_(f"Writing export report to {report}")
write_export_report(report, results)
photo_str_total = "photos" if len(photos) != 1 else "photo"
if update:
summary = (
@@ -1891,6 +1859,42 @@ def export(
else:
click.echo("Did not find any photos to export")
# cleanup files and do report if needed
if cleanup:
all_files = (
results.exported
+ results.skipped
+ results.exif_updated
+ results.touched
+ results.converted_to_jpeg
+ results.sidecar_json_written
+ results.sidecar_json_skipped
+ results.sidecar_exiftool_written
+ results.sidecar_exiftool_skipped
+ results.sidecar_xmp_written
+ results.sidecar_xmp_skipped
# include missing so a file that was already in export directory
# but was missing on --update doesn't get deleted
# (better to have old version than none)
+ results.missing
# include files that have error in case they exist from previous export
+ [r[0] for r in results.error]
+ [str(pathlib.Path(export_db_path).resolve())]
)
click.echo(f"Cleaning up {dest}")
cleaned_files, cleaned_dirs = cleanup_files(dest, all_files, fileutil)
file_str = "files" if len(cleaned_files) != 1 else "file"
dir_str = "directories" if len(cleaned_dirs) != 1 else "directory"
click.echo(
f"Deleted: {len(cleaned_files)} {file_str}, {len(cleaned_dirs)} {dir_str}"
)
results.deleted_files = cleaned_files
results.deleted_directories = cleaned_dirs
if report:
verbose_(f"Writing export report to {report}")
write_export_report(report, results)
export_db.close()

View File

@@ -55,7 +55,8 @@ from ..photokit import (
PhotoLibrary,
)
from ..phototemplate import RenderOptions
from ..utils import findfiles, get_preferred_uti_extension, lineno, noop
from ..uti import get_preferred_uti_extension
from ..utils import findfiles, lineno, noop
# retry if use_photos_export fails the first time (which sometimes it does)
MAX_PHOTOSCRIPT_RETRIES = 3

View File

@@ -36,7 +36,8 @@ from ..albuminfo import AlbumInfo, ImportInfo
from ..personinfo import FaceInfo, PersonInfo
from ..phototemplate import PhotoTemplate, RenderOptions
from ..placeinfo import PlaceInfo4, PlaceInfo5
from ..utils import _debug, _get_resource_loc, findfiles, get_preferred_uti_extension
from ..uti import get_preferred_uti_extension, get_uti_for_extension
from ..utils import _debug, _get_resource_loc, findfiles
class PhotoInfo:
@@ -389,27 +390,23 @@ class PhotoInfo:
photopath = None
else:
filestem = pathlib.Path(self._info["filename"]).stem
raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
# raw_ext = get_preferred_uti_extension(self._info["UTI_raw"])
if self._info["directory"].startswith("/"):
filepath = self._info["directory"]
else:
filepath = os.path.join(self._db._masters_path, self._info["directory"])
glob_str = f"{filestem}*.{raw_ext}"
# raw files have same name as original but with _4.raw_ext appended
# I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4
# see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc
glob_str = f"{filestem}_4*"
raw_file = findfiles(glob_str, filepath)
if len(raw_file) != 1:
# Note: In Photos Version 5.0 (141.19.150), images not copied to Photos Library
# that are missing do not always trigger is_missing = True as happens
# in earlier version so it's possible for this check to fail, if so, return None
logging.debug(f"Error getting path to RAW file: {filepath}/{glob_str}")
if not raw_file:
photopath = None
else:
photopath = os.path.join(filepath, raw_file[0])
if not os.path.isfile(photopath):
logging.debug(
f"MISSING PATH: RAW photo for UUID {self._uuid} should be at {photopath} but does not appear to exist"
)
photopath = None
return photopath
@@ -661,13 +658,23 @@ class PhotoInfo:
"""Returns Uniform Type Identifier (UTI) for the original image
for example: public.jpeg or com.apple.quicktime-movie
"""
if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
return self._info["raw_pair_info"]["UTI"]
elif self.shared:
# TODO: need reliable way to get original UTI for shared
return self.uti
else:
return self._info["UTI_original"]
try:
return self._uti_original
except AttributeError:
if self._db._db_version <= _PHOTOS_4_VERSION and self._info["has_raw"]:
self._uti_original = self._info["raw_pair_info"]["UTI"]
elif self.shared:
# TODO: need reliable way to get original UTI for shared
self._uti_original = self.uti
elif self._db._photos_ver >= 7:
# Monterey+
self._uti_original = get_uti_for_extension(
pathlib.Path(self.original_filename).suffix
)
else:
self._uti_original = self._info["UTI_original"]
return self._uti_original
@property
def uti_edited(self):
@@ -686,7 +693,14 @@ class PhotoInfo:
for example: com.canon.cr2-raw-image
Returns None if no associated RAW image
"""
return self._info["UTI_raw"]
if self._db._photos_ver < 7:
return self._info["UTI_raw"]
rawpath = self.path_raw
if rawpath:
return get_uti_for_extension(pathlib.Path(rawpath).suffix)
else:
return None
@property
def ismovie(self):

View File

@@ -34,7 +34,8 @@ from Foundation import NSNotificationCenter, NSObject
from PyObjCTools import AppHelper
from .fileutil import FileUtil
from .utils import _get_os_version, get_preferred_uti_extension, increment_filename
from .uti import get_preferred_uti_extension
from .utils import _get_os_version, increment_filename
# NOTE: This requires user have granted access to the terminal (e.g. Terminal.app or iTerm)
# to access Photos. This should happen automatically the first time it's called. I've
@@ -64,7 +65,7 @@ MIN_SLEEP = 0.015
### utility functions
def NSURL_to_path(url):
""" Convert URL string as represented by NSURL to a path string """
"""Convert URL string as represented by NSURL to a path string"""
nsurl = Foundation.NSURL.alloc().initWithString_(
Foundation.NSString.alloc().initWithString_(str(url))
)
@@ -74,7 +75,7 @@ def NSURL_to_path(url):
def path_to_NSURL(path):
""" Convert path string to NSURL """
"""Convert path string to NSURL"""
pathstr = Foundation.NSString.alloc().initWithString_(str(path))
url = Foundation.NSURL.fileURLWithPath_(pathstr)
pathstr.dealloc()
@@ -82,10 +83,10 @@ def path_to_NSURL(path):
def check_photokit_authorization():
""" Check authorization to use user's Photos Library
"""Check authorization to use user's Photos Library
Returns:
True if user has authorized access to the Photos library, otherwise False
True if user has authorized access to the Photos library, otherwise False
"""
auth_status = Photos.PHPhotoLibrary.authorizationStatus()
@@ -93,7 +94,7 @@ def check_photokit_authorization():
def request_photokit_authorization():
""" Request authorization to user's Photos Library
"""Request authorization to user's Photos Library
Returns:
authorization status
@@ -135,39 +136,39 @@ def request_photokit_authorization():
### exceptions
class PhotoKitError(Exception):
"""Base class for exceptions in this module. """
"""Base class for exceptions in this module."""
pass
class PhotoKitFetchFailed(PhotoKitError):
"""Exception raised for errors in the input. """
"""Exception raised for errors in the input."""
pass
class PhotoKitAuthError(PhotoKitError):
"""Exception raised if unable to authorize use of PhotoKit. """
"""Exception raised if unable to authorize use of PhotoKit."""
pass
class PhotoKitExportError(PhotoKitError):
"""Exception raised if unable to export asset. """
"""Exception raised if unable to export asset."""
pass
class PhotoKitMediaTypeError(PhotoKitError):
""" Exception raised if an unknown mediaType() is encountered """
"""Exception raised if an unknown mediaType() is encountered"""
pass
### helper classes
class ImageData:
""" Simple class to hold the data passed to the handler for
requestImageDataAndOrientationForAsset_options_resultHandler_
"""Simple class to hold the data passed to the handler for
requestImageDataAndOrientationForAsset_options_resultHandler_
"""
def __init__(
@@ -181,8 +182,7 @@ class ImageData:
class AVAssetData:
""" Simple class to hold the data passed to the handler for
"""
"""Simple class to hold the data passed to the handler for"""
def __init__(self):
self.asset = None
@@ -192,7 +192,7 @@ class AVAssetData:
class PHAssetResourceData:
""" Simple class to hold data from
"""Simple class to hold data from
requestDataForAssetResource:options:dataReceivedHandler:completionHandler:
"""
@@ -211,8 +211,8 @@ class PHAssetResourceData:
class PhotoKitNotificationDelegate(NSObject):
""" Handles notifications from NotificationCenter;
used with asynchronous PhotoKit requests to stop event loop when complete
"""Handles notifications from NotificationCenter;
used with asynchronous PhotoKit requests to stop event loop when complete
"""
def liveNotification_(self, note):
@@ -226,11 +226,11 @@ class PhotoKitNotificationDelegate(NSObject):
### main class implementation
class PhotoAsset:
""" PhotoKit PHAsset representation """
"""PhotoKit PHAsset representation"""
def __init__(self, manager, phasset):
""" Return a PhotoAsset object
"""Return a PhotoAsset object
Args:
manager = ImageManager object
phasset: a PHAsset object
@@ -241,32 +241,32 @@ class PhotoAsset:
@property
def phasset(self):
""" Return PHAsset instance """
"""Return PHAsset instance"""
return self._phasset
@property
def uuid(self):
""" Return local identifier (UUID) of PHAsset """
"""Return local identifier (UUID) of PHAsset"""
return self._phasset.localIdentifier()
@property
def isphoto(self):
""" Return True if asset is photo (image), otherwise False """
"""Return True if asset is photo (image), otherwise False"""
return self.media_type == Photos.PHAssetMediaTypeImage
@property
def ismovie(self):
""" Return True if asset is movie (video), otherwise False """
"""Return True if asset is movie (video), otherwise False"""
return self.media_type == Photos.PHAssetMediaTypeVideo
@property
def isaudio(self):
""" Return True if asset is audio, otherwise False """
"""Return True if asset is audio, otherwise False"""
return self.media_type == Photos.PHAssetMediaTypeAudio
@property
def original_filename(self):
""" Return original filename asset was imported with """
"""Return original filename asset was imported with"""
resources = self._resources()
for resource in resources:
if (
@@ -278,10 +278,22 @@ class PhotoAsset:
return resource.originalFilename()
return None
@property
def raw_filename(self):
"""Return RAW filename for RAW+JPEG photos or None if no RAW asset"""
resources = self._resources()
for resource in resources:
if (
self.isphoto
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
):
return resource.originalFilename()
return None
@property
def hasadjustments(self):
""" Check to see if a PHAsset has adjustment data associated with it
Returns False if no adjustments, True if any adjustments """
"""Check to see if a PHAsset has adjustment data associated with it
Returns False if no adjustments, True if any adjustments"""
# reference: https://developer.apple.com/documentation/photokit/phassetresource/1623988-assetresourcesforasset?language=objc
@@ -298,112 +310,112 @@ class PhotoAsset:
@property
def media_type(self):
""" media type such as image or video """
"""media type such as image or video"""
return self.phasset.mediaType()
@property
def media_subtypes(self):
""" media subtype """
"""media subtype"""
return self.phasset.mediaSubtypes()
@property
def panorama(self):
""" return True if asset is panorama, otherwise False """
"""return True if asset is panorama, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoPanorama)
@property
def hdr(self):
""" return True if asset is HDR, otherwise False """
"""return True if asset is HDR, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoHDR)
@property
def screenshot(self):
""" return True if asset is screenshot, otherwise False """
"""return True if asset is screenshot, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoScreenshot)
@property
def live(self):
""" return True if asset is live, otherwise False """
"""return True if asset is live, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoLive)
@property
def streamed(self):
""" return True if asset is streamed video, otherwise False """
"""return True if asset is streamed video, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoStreamed)
@property
def slow_mo(self):
""" return True if asset is slow motion (high frame rate) video, otherwise False """
"""return True if asset is slow motion (high frame rate) video, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoHighFrameRate)
@property
def time_lapse(self):
""" return True if asset is time lapse video, otherwise False """
"""return True if asset is time lapse video, otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypeVideoTimelapse)
@property
def portrait(self):
""" return True if asset is portrait (depth effect), otherwise False """
"""return True if asset is portrait (depth effect), otherwise False"""
return bool(self.media_subtypes & Photos.PHAssetMediaSubtypePhotoDepthEffect)
@property
def burstid(self):
""" return burstIdentifier of image if image is burst photo otherwise None """
"""return burstIdentifier of image if image is burst photo otherwise None"""
return self.phasset.burstIdentifier()
@property
def burst(self):
""" return True if image is burst otherwise False """
"""return True if image is burst otherwise False"""
return bool(self.burstid)
@property
def source_type(self):
""" the means by which the asset entered the user's library """
"""the means by which the asset entered the user's library"""
return self.phasset.sourceType()
@property
def pixel_width(self):
""" width in pixels """
"""width in pixels"""
return self.phasset.pixelWidth()
@property
def pixel_height(self):
""" height in pixels """
"""height in pixels"""
return self.phasset.pixelHeight()
@property
def date(self):
""" date asset was created """
"""date asset was created"""
return self.phasset.creationDate()
@property
def date_modified(self):
""" date asset was modified """
"""date asset was modified"""
return self.phasset.modificationDate()
@property
def location(self):
""" location of the asset """
"""location of the asset"""
return self.phasset.location()
@property
def duration(self):
""" duration of the asset """
"""duration of the asset"""
return self.phasset.duration()
@property
def favorite(self):
""" True if asset is favorite, otherwise False """
"""True if asset is favorite, otherwise False"""
return self.phasset.isFavorite()
@property
def hidden(self):
""" True if asset is hidden, otherwise False """
"""True if asset is hidden, otherwise False"""
return self.phasset.isHidden()
def metadata(self, version=PHOTOS_VERSION_CURRENT):
""" Return dict of asset metadata
"""Return dict of asset metadata
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
@@ -411,17 +423,28 @@ class PhotoAsset:
return imagedata.metadata
def uti(self, version=PHOTOS_VERSION_CURRENT):
""" Return UTI of asset
"""Return UTI of asset
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
imagedata = self._request_image_data(version=version)
return imagedata.uti
def uti_raw(self):
"""Return UTI of RAW component of RAW+JPEG pair"""
resources = self._resources()
for resource in resources:
if (
self.isphoto
and resource.type() == Photos.PHAssetResourceTypeAlternatePhoto
):
return resource.uniformTypeIdentifier()
return None
def url(self, version=PHOTOS_VERSION_CURRENT):
""" Return URL of asset
"""Return URL of asset
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
@@ -429,8 +452,8 @@ class PhotoAsset:
return str(imagedata.info["PHImageFileURLKey"])
def path(self, version=PHOTOS_VERSION_CURRENT):
""" Return path of asset
"""Return path of asset
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
@@ -439,8 +462,8 @@ class PhotoAsset:
return url.fileSystemRepresentation().decode("utf-8")
def orientation(self, version=PHOTOS_VERSION_CURRENT):
""" Return orientation of asset
"""Return orientation of asset
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
@@ -449,8 +472,8 @@ class PhotoAsset:
@property
def degraded(self, version=PHOTOS_VERSION_CURRENT):
""" Return True if asset is degraded version
"""Return True if asset is degraded version
Args:
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
"""
@@ -458,15 +481,21 @@ class PhotoAsset:
return imagedata.info["PHImageResultIsDegradedKey"]
def export(
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
self,
dest,
filename=None,
version=PHOTOS_VERSION_CURRENT,
overwrite=False,
raw=False,
):
""" Export image to path
"""Export image to path
Args:
dest: str, path to destination directory
filename: str, optional name of exported file; if not provided, defaults to asset's original filename
version: which version of image (PHOTOS_VERSION_ORIGINAL or PHOTOS_VERSION_CURRENT)
overwrite: bool, if True, overwrites destination file if it already exists; default is False
raw: bool, if True, export RAW component of RAW+JPEG pair, default is False
Returns:
List of path to exported image(s)
@@ -491,11 +520,28 @@ class PhotoAsset:
output_file = None
if self.isphoto:
imagedata = self._request_image_data(version=version)
if not imagedata.image_data:
raise PhotoKitExportError("Could not get image data")
ext = get_preferred_uti_extension(imagedata.uti)
# will hold exported image data and needs to be cleaned up at end
imagedata = None
if raw:
# export the raw component
resources = self._resources()
for resource in resources:
if resource.type() == Photos.PHAssetResourceTypeAlternatePhoto:
data = self._request_resource_data(resource)
ext = pathlib.Path(self.raw_filename).suffix[1:]
break
else:
raise PhotoKitExportError(
"Could not get image data for RAW photo"
)
else:
# TODO: if user has selected use RAW as original, this returns the RAW
# can get the jpeg with resource.type() == Photos.PHAssetResourceTypePhoto
imagedata = self._request_image_data(version=version)
if not imagedata.image_data:
raise PhotoKitExportError("Could not get image data")
ext = get_preferred_uti_extension(imagedata.uti)
data = imagedata.image_data
output_file = dest / f"{filename.stem}.{ext}"
@@ -503,7 +549,9 @@ class PhotoAsset:
output_file = pathlib.Path(increment_filename(output_file))
with open(output_file, "wb") as fd:
fd.write(imagedata.image_data)
fd.write(data)
if imagedata:
del imagedata
elif self.ismovie:
videodata = self._request_video_data(version=version)
@@ -525,14 +573,14 @@ class PhotoAsset:
return [str(output_file)]
def _request_image_data(self, version=PHOTOS_VERSION_ORIGINAL):
""" Request image data and metadata for self._phasset
"""Request image data and metadata for self._phasset
Args:
version: which version to request
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
PHOTOS_VERSION_CURRENT, request current version with all edits
PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version
Returns:
ImageData instance
@@ -562,8 +610,8 @@ class PhotoAsset:
event = threading.Event()
def handler(imageData, dataUTI, orientation, info):
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
"""result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object)"""
nonlocal requestdata
@@ -593,19 +641,63 @@ class PhotoAsset:
del requestdata
return data
def _request_resource_data(self, resource):
"""Request asset resource data (either photo or video component)
Args:
resource: PHAssetResource to request
Raises:
"""
with objc.autorelease_pool():
resource_manager = Photos.PHAssetResourceManager.defaultManager()
options = Photos.PHAssetResourceRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True)
requestdata = PHAssetResourceData()
event = threading.Event()
def handler(data):
"""result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object)"""
nonlocal requestdata
requestdata.data += data
def completion_handler(error):
if error:
raise PhotoKitExportError(
"Error requesting data for asset resource"
)
event.set()
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
resource, options, handler, completion_handler
)
event.wait()
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata.data)
del requestdata
return data
def _make_result_handle_(self, data):
""" Make handler function and threading event to use with
requestImageDataAndOrientationForAsset_options_resultHandler_
data: Fetchdata class to hold resulting metadata
returns: handler function, threading.Event() instance
Following call to requestImageDataAndOrientationForAsset_options_resultHandler_,
data will hold data from the fetch """
"""Make handler function and threading event to use with
requestImageDataAndOrientationForAsset_options_resultHandler_
data: Fetchdata class to hold resulting metadata
returns: handler function, threading.Event() instance
Following call to requestImageDataAndOrientationForAsset_options_resultHandler_,
data will hold data from the fetch"""
event = threading.Event()
def handler(imageData, dataUTI, orientation, info):
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
"""result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object)"""
nonlocal data
@@ -626,14 +718,14 @@ class PhotoAsset:
return handler, event
def _resources(self):
""" Return list of PHAssetResource for object """
"""Return list of PHAssetResource for object"""
resources = Photos.PHAssetResource.assetResourcesForAsset_(self.phasset)
return [resources.objectAtIndex_(idx) for idx in range(resources.count())]
class SlowMoVideoExporter(NSObject):
def initWithAVAsset_path_(self, avasset, path):
""" init helper class for exporting slow-mo video
"""init helper class for exporting slow-mo video
Args:
avasset: AVAsset
@@ -648,15 +740,17 @@ class SlowMoVideoExporter(NSObject):
return self
def exportSlowMoVideo(self):
""" export slow-mo video with AVAssetExportSession
"""export slow-mo video with AVAssetExportSession
Returns:
path to exported file
"""
with objc.autorelease_pool():
exporter = AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
exporter = (
AVFoundation.AVAssetExportSession.alloc().initWithAsset_presetName_(
self.avasset, AVFoundation.AVAssetExportPresetHighestQuality
)
)
exporter.setOutputURL_(self.url)
exporter.setOutputFileType_(AVFoundation.AVFileTypeQuickTimeMovie)
@@ -665,7 +759,7 @@ class SlowMoVideoExporter(NSObject):
self.done = False
def handler():
""" result handler for exportAsynchronouslyWithCompletionHandler """
"""result handler for exportAsynchronouslyWithCompletionHandler"""
self.done = True
exporter.exportAsynchronouslyWithCompletionHandler_(handler)
@@ -699,7 +793,7 @@ class SlowMoVideoExporter(NSObject):
class VideoAsset(PhotoAsset):
""" PhotoKit PHAsset representation of video asset """
"""PhotoKit PHAsset representation of video asset"""
# TODO: doesn't work for slow-mo videos
# see https://stackoverflow.com/questions/26152396/how-to-access-nsdata-nsurl-of-slow-motion-videos-using-photokit
@@ -709,7 +803,7 @@ class VideoAsset(PhotoAsset):
def export(
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
):
""" Export video to path
"""Export video to path
Args:
dest: str, path to destination directory
@@ -765,7 +859,7 @@ class VideoAsset(PhotoAsset):
def _export_slow_mo(
self, dest, filename=None, version=PHOTOS_VERSION_CURRENT, overwrite=False
):
""" Export slow-motion video to path
"""Export slow-motion video to path
Args:
dest: str, path to destination directory
@@ -814,14 +908,14 @@ class VideoAsset(PhotoAsset):
# todo: rewrite this with NotificationCenter and App event loop?
def _request_video_data(self, version=PHOTOS_VERSION_ORIGINAL):
""" Request video data for self._phasset
"""Request video data for self._phasset
Args:
version: which version to request
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
PHOTOS_VERSION_ORIGINAL (default), request original highest fidelity version
PHOTOS_VERSION_CURRENT, request current version with all edits
PHOTOS_VERSION_UNADJUSTED, request highest quality unadjusted version
Raises:
ValueError if passed invalid value for version
"""
@@ -843,7 +937,7 @@ class VideoAsset(PhotoAsset):
event = threading.Event()
def handler(asset, audiomix, info):
""" result handler for requestAVAssetForVideo:asset options:options resultHandler """
"""result handler for requestAVAssetForVideo:asset options:options resultHandler"""
nonlocal requestdata
requestdata.asset = asset
@@ -865,8 +959,8 @@ class VideoAsset(PhotoAsset):
class LivePhotoRequest(NSObject):
""" Manage requests for live photo assets
See: https://developer.apple.com/documentation/photokit/phimagemanager/1616984-requestlivephotoforasset?language=objc
"""Manage requests for live photo assets
See: https://developer.apple.com/documentation/photokit/phimagemanager/1616984-requestlivephotoforasset?language=objc
"""
def initWithManager_Asset_(self, manager, asset):
@@ -879,7 +973,7 @@ class LivePhotoRequest(NSObject):
return self
def requestLivePhotoResources(self, version=PHOTOS_VERSION_CURRENT):
""" return the photos and video components of a live video as [PHAssetResource] """
"""return the photos and video components of a live video as [PHAssetResource]"""
with objc.autorelease_pool():
options = Photos.PHLivePhotoRequestOptions.alloc().init()
@@ -897,7 +991,7 @@ class LivePhotoRequest(NSObject):
self.live_photo = None
def handler(result, info):
""" result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler: """
"""result handler for requestLivePhotoForAsset:targetSize:contentMode:options:resultHandler:"""
if not info["PHImageResultIsDegradedKey"]:
self.live_photo = result
self.info = info
@@ -939,7 +1033,7 @@ class LivePhotoRequest(NSObject):
class LivePhotoAsset(PhotoAsset):
""" Represents a live photo """
"""Represents a live photo"""
def export(
self,
@@ -950,7 +1044,7 @@ class LivePhotoAsset(PhotoAsset):
photo=True,
video=True,
):
""" Export image to path
"""Export image to path
Args:
dest: str, path to destination directory
@@ -1061,50 +1155,6 @@ class LivePhotoAsset(PhotoAsset):
request.dealloc()
return exported
def _request_resource_data(self, resource):
""" Request asset resource data (either photo or video component)
Args:
resource: PHAssetResource to request
Raises:
"""
with objc.autorelease_pool():
resource_manager = Photos.PHAssetResourceManager.defaultManager()
options = Photos.PHAssetResourceRequestOptions.alloc().init()
options.setNetworkAccessAllowed_(True)
requestdata = PHAssetResourceData()
event = threading.Event()
def handler(data):
""" result handler for requestImageDataAndOrientationForAsset_options_resultHandler_
all returned by the request is set as properties of nonlocal data (Fetchdata object) """
nonlocal requestdata
requestdata.data += data
def completion_handler(error):
if error:
raise PhotoKitExportError(
"Error requesting data for asset resource"
)
event.set()
resource_manager.requestDataForAssetResource_options_dataReceivedHandler_completionHandler_(
resource, options, handler, completion_handler
)
event.wait()
# not sure why this is needed -- some weird ref count thing maybe
# if I don't do this, memory leaks
data = copy.copy(requestdata.data)
del requestdata
return data
# def request_image_data(self, version=PHOTOS_VERSION_CURRENT):
# # Returns an NSImage which isn't overly useful
# # https://developer.apple.com/documentation/photokit/phimagemanager/1616964-requestimageforasset?language=objc
@@ -1142,12 +1192,12 @@ class LivePhotoAsset(PhotoAsset):
class PhotoLibrary:
""" Interface to PhotoKit PHImageManager and PHPhotoLibrary """
"""Interface to PhotoKit PHImageManager and PHPhotoLibrary"""
def __init__(self):
""" Initialize ImageManager instance. Requests authorization to use the
"""Initialize ImageManager instance. Requests authorization to use the
Photos library if authorization has not already been granted.
Raises:
PhotoKitAuthError if unable to authorize access to PhotoKit
"""
@@ -1166,7 +1216,7 @@ class PhotoLibrary:
self._phimagemanager = Photos.PHCachingImageManager.defaultManager()
def request_authorization(self):
""" Request authorization to user's Photos Library
"""Request authorization to user's Photos Library
Returns:
authorization status
@@ -1176,7 +1226,7 @@ class PhotoLibrary:
return self.auth_status
def fetch_uuid_list(self, uuid_list):
""" fetch PHAssets with uuids in uuid_list
"""fetch PHAssets with uuids in uuid_list
Args:
uuid_list: list of str (UUID of image assets to fetch)
@@ -1205,7 +1255,7 @@ class PhotoLibrary:
)
def fetch_uuid(self, uuid):
""" fetch PHAsset with uuid = uuid
"""fetch PHAsset with uuid = uuid
Args:
uuid: str; UUID of image asset to fetch
@@ -1223,8 +1273,8 @@ class PhotoLibrary:
raise PhotoKitFetchFailed(f"Fetch did not return result for uuid {uuid}")
def fetch_burst_uuid(self, burstid, all=False):
""" fetch PhotoAssets with burst ID = burstid
"""fetch PhotoAssets with burst ID = burstid
Args:
burstid: str, burst UUID
all: return all burst assets; if False returns only those selected by the user (including the "key photo" even if user hasn't manually selected it)
@@ -1253,11 +1303,11 @@ class PhotoLibrary:
)
def _asset_factory(self, phasset):
""" creates a PhotoAsset, VideoAsset, or LivePhotoAsset
"""creates a PhotoAsset, VideoAsset, or LivePhotoAsset
Args:
phasset: PHAsset object
phasset: PHAsset object
Returns:
PhotoAsset, VideoAsset, or LivePhotoAsset depending on type of PHAsset
"""

View File

@@ -599,7 +599,9 @@ class PhotosDB:
verbose = self._verbose
verbose("Processing database.")
verbose(f"Database version: {self._db_version}.")
self._photos_ver = 4 # only used in Photos 5+
(conn, c) = _open_sql_file(self._tmp_db)
# get info to associate persons with photos
@@ -1595,10 +1597,14 @@ class PhotosDB:
verbose(f"Database version: {self._db_version}, {photos_ver}.")
asset_table = _DB_TABLE_NAMES[photos_ver]["ASSET"]
keyword_join = _DB_TABLE_NAMES[photos_ver]["KEYWORD_JOIN"]
asset_album_table = _DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_TABLE"]
album_join = _DB_TABLE_NAMES[photos_ver]["ALBUM_JOIN"]
album_sort = _DB_TABLE_NAMES[photos_ver]["ALBUM_SORT_ORDER"]
asset_album_join = _DB_TABLE_NAMES[photos_ver]["ASSET_ALBUM_JOIN"]
import_fok = _DB_TABLE_NAMES[photos_ver]["IMPORT_FOK"]
depth_state = _DB_TABLE_NAMES[photos_ver]["DEPTH_STATE"]
uti_original_column = _DB_TABLE_NAMES[photos_ver]["UTI_ORIGINAL"]
hdr_type_column = _DB_TABLE_NAMES[photos_ver]["HDR_TYPE"]
# Look for all combinations of persons and pictures
if _debug():
@@ -1718,8 +1724,8 @@ class PhotosDB:
{asset_table}.ZUUID,
{album_sort}
FROM {asset_table}
JOIN Z_26ASSETS ON {album_join} = {asset_table}.Z_PK
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = Z_26ASSETS.Z_26ALBUMS
JOIN {asset_album_table} ON {album_join} = {asset_table}.Z_PK
JOIN ZGENERICALBUM ON ZGENERICALBUM.Z_PK = {asset_album_join}
"""
)
@@ -1886,7 +1892,7 @@ class PhotosDB:
{asset_table}.ZAVALANCHEUUID,
{asset_table}.ZAVALANCHEPICKTYPE,
{asset_table}.ZKINDSUBTYPE,
{asset_table}.ZCUSTOMRENDEREDVALUE,
{asset_table}.{hdr_type_column},
ZADDITIONALASSETATTRIBUTES.ZCAMERACAPTUREDEVICE,
{asset_table}.ZCLOUDASSETGUID,
ZADDITIONALASSETATTRIBUTES.ZREVERSELOCATIONDATA,
@@ -2250,20 +2256,33 @@ class PhotosDB:
# Get info on remote/local availability for photos in shared albums
# Also get UTI of original image (zdatastoresubtype = 1)
c.execute(
f""" SELECT
if self._photos_ver >= 7:
sql_missing = f""" SELECT
{asset_table}.ZUUID,
ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,
ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER,
{uti_original_column},
null
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
else:
sql_missing = f""" SELECT
{asset_table}.ZUUID,
ZINTERNALRESOURCE.ZLOCALAVAILABILITY,
ZINTERNALRESOURCE.ZREMOTEAVAILABILITY,
ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
{uti_original_column},
ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER
FROM {asset_table}
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER
WHERE ZDATASTORESUBTYPE = 1 OR ZDATASTORESUBTYPE = 3 """
)
c.execute(sql_missing)
# Order of results:
# 0 {asset_table}.ZUUID,
@@ -2323,8 +2342,20 @@ class PhotosDB:
# get information about associted RAW images
# RAW images have ZDATASTORESUBTYPE = 17
c.execute(
f""" SELECT
if self._photos_ver >= 7:
sql_raw = f""" SELECT
{asset_table}.ZUUID,
ZINTERNALRESOURCE.ZDATALENGTH,
null,
ZINTERNALRESOURCE.ZDATASTORESUBTYPE,
ZINTERNALRESOURCE.ZRESOURCETYPE
FROM {asset_table}
JOIN ZINTERNALRESOURCE ON ZINTERNALRESOURCE.ZASSET = ZADDITIONALASSETATTRIBUTES.ZASSET
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17
"""
else:
sql_raw = f""" SELECT
{asset_table}.ZUUID,
ZINTERNALRESOURCE.ZDATALENGTH,
ZUNIFORMTYPEIDENTIFIER.ZIDENTIFIER,
@@ -2335,8 +2366,10 @@ class PhotosDB:
JOIN ZADDITIONALASSETATTRIBUTES ON ZADDITIONALASSETATTRIBUTES.ZASSET = {asset_table}.Z_PK
JOIN ZUNIFORMTYPEIDENTIFIER ON ZUNIFORMTYPEIDENTIFIER.Z_PK = ZINTERNALRESOURCE.ZUNIFORMTYPEIDENTIFIER
WHERE ZINTERNALRESOURCE.ZDATASTORESUBTYPE = 17
"""
)
"""
c.execute(sql_raw)
for row in c:
uuid = row[0]
if uuid in self._dbphotos:

View File

@@ -6,6 +6,7 @@ import plistlib
from .._constants import (
_PHOTOS_5_MODEL_VERSION,
_PHOTOS_6_MODEL_VERSION,
_PHOTOS_7_MODEL_VERSION,
_TESTED_DB_VERSIONS,
)
from ..utils import _open_sql_file
@@ -73,12 +74,12 @@ def get_db_model_version(db_file):
model_ver = get_model_version(db_file)
if _PHOTOS_5_MODEL_VERSION[0] <= model_ver <= _PHOTOS_5_MODEL_VERSION[1]:
db_ver = 5
return 5
elif _PHOTOS_6_MODEL_VERSION[0] <= model_ver <= _PHOTOS_6_MODEL_VERSION[1]:
db_ver = 6
return 6
elif _PHOTOS_7_MODEL_VERSION[0] <= model_ver <= _PHOTOS_7_MODEL_VERSION[1]:
return 7
else:
logging.warning(f"Unknown model version: {model_ver}")
# cross our fingers and try latest version
db_ver = 6
return db_ver
return 7

View File

@@ -4,8 +4,10 @@ import datetime
import locale
import os
import pathlib
import sys
import shlex
import sys
from dataclasses import dataclass
from typing import Optional
from textx import TextXSyntaxError, metamodel_from_file
@@ -14,9 +16,7 @@ from ._version import __version__
from .datetime_formatter import DateTimeFormatter
from .exiftool import ExifToolCaching
from .path_utils import sanitize_dirname, sanitize_filename, sanitize_pathpart
from .utils import load_function
from dataclasses import dataclass
from typing import Optional
from .utils import expand_and_validate_filepath, load_function
# TODO: a lot of values are passed from function to function like path_sep--make these all class properties
@@ -1177,10 +1177,11 @@ class PhotoTemplate:
filename, funcname = subfield.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
template_func = load_function(filename_validated, funcname)
values = template_func(self.photo)
if not isinstance(values, (str, list)):
@@ -1211,10 +1212,11 @@ class PhotoTemplate:
filename, funcname = filter_.split("::")
if not pathlib.Path(filename).is_file():
filename_validated = expand_and_validate_filepath(filename)
if not filename_validated:
raise ValueError(f"'{filename}' does not appear to be a file")
template_func = load_function(filename, funcname)
template_func = load_function(filename_validated, funcname)
if not isinstance(values, (list, tuple)):
values = [values]

View File

@@ -63,7 +63,8 @@ SubField:
;
SUBFIELD_WORD:
/[\.\w:\/]+/
/[\.\w:\/\-\~\'\"\%\@\#\^\]+/
/\\\s/?
;
Filter:

621
osxphotos/uti.py Normal file
View File

@@ -0,0 +1,621 @@
""" get UTI for a given file extension and the preferred extension for a given UTI """
""" Implementation note: runs only on macOS
On macOS <= 11 (Big Sur), uses objective C CoreServices methods
UTTypeCopyPreferredTagWithClass and UTTypeCreatePreferredIdentifierForTag to retrieve the
UTI and the extension. These are deprecated in 10.15 (Catalina) and no longer supported on Monterey.
On Monterey, these calls are replaced with Swift methods that I can't call from python so
this code uses a cached dict of UTI values. The code first checks to see if the extension or UTI
is available in the cache and if so, returns it. If not, it performs a subprocess call to `mdls` to
retrieve the UTI (by creating a temp file with the correct extension) and returns the UTI. This only
works for the extension -> UTI lookup. On Monterey, if there is no cached value for UTI -> extension lookup,
returns None.
It's a bit hacky but best I can think of to make this robust on different versions of macOS. PRs welcome.
"""
import csv
import re
import subprocess
import tempfile
import CoreServices
import objc
from .utils import _get_os_version
# cached values of all the UTIs (< 6 chars long) known to my Mac running macOS 10.15.7
UTI_CSV = """extension,UTI,preferred_extension,MIME_type
c,public.c-source,c,None
f,public.fortran-source,f,None
h,public.c-header,h,None
i,public.c-source.preprocessed,i,None
l,public.lex-source,l,None
m,public.objective-c-source,m,None
o,public.object-code,o,None
r,com.apple.rez-source,r,None
s,public.assembly-source,s,None
y,public.yacc-source,y,None
z,public.z-archive,z,application/x-compress
aa,com.audible.aa-audiobook,aa,audio/audible
ai,com.adobe.illustrator.ai-image,ai,None
as,com.apple.applesingle-archive,as,None
au,public.au-audio,au,audio/basic
bz,public.bzip2-archive,bz2,application/x-bzip2
cc,public.c-plus-plus-source,cp,None
cp,public.c-plus-plus-source,cp,None
dv,public.dv-movie,dv,video/x-dv
gz,org.gnu.gnu-zip-archive,gz,application/x-gzip
hh,public.c-plus-plus-header,hh,None
hp,public.c-plus-plus-header,hh,None
ii,public.c-plus-plus-source.preprocessed,ii,None
js,com.netscape.javascript-source,js,text/javascript
lm,public.lex-source,l,None
mi,public.objective-c-source.preprocessed,mi,None
mm,public.objective-c-plus-plus-source,mm,None
pf,com.apple.colorsync-profile,icc,None
pl,public.perl-script,pl,text/x-perl-script
pm,public.perl-script,pl,text/x-perl-script
ps,com.adobe.postscript,ps,application/postscript
py,public.python-script,py,text/x-python-script
qt,com.apple.quicktime-movie,mov,video/quicktime
ra,com.real.realaudio,ram,audio/vnd.rn-realaudio
rb,public.ruby-script,rb,text/x-ruby-script
rm,com.real.realmedia,rm,application/vnd.rn-realmedia
sh,public.shell-script,sh,None
ts,public.mpeg-2-transport-stream,ts,None
ul,public.ulaw-audio,ul,None
uu,public.uuencoded-archive,uu,text/x-uuencode
wm,com.microsoft.windows-media-wm,wm,video/x-ms-wm
ym,public.yacc-source,y,None
aac,public.aac-audio,aac,audio/aac
aae,com.apple.photos.apple-adjustment-envelope,aae,None
aaf,org.aafassociation.advanced-authoring-format,aaf,None
aax,com.audible.aax-audiobook,aax,audio/vnd.audible.aax
abc,public.alembic,abc,None
ac3,public.ac3-audio,ac3,audio/ac3
ada,public.ada-source,ada,None
adb,public.ada-source,ada,None
ads,public.ada-source,ada,None
aif,public.aifc-audio,aifc,audio/aiff
amr,org.3gpp.adaptive-multi-rate-audio,amr,audio/amr
app,com.apple.application-bundle,app,None
arw,com.sony.arw-raw-image,arw,None
asf,com.microsoft.advanced-systems-format,asf,video/x-ms-asf
asx,com.microsoft.advanced-stream-redirector,asx,video/x-ms-asx
avi,public.avi,avi,video/avi
bdm,public.avchd-content,bdm,None
bin,com.apple.macbinary-archive,bin,application/macbinary
bmp,com.microsoft.bmp,bmp,image/bmp
bwf,com.microsoft.waveform-audio,wav,audio/vnd.wave
bz2,public.bzip2-archive,bz2,application/x-bzip2
caf,com.apple.coreaudio-format,caf,None
cdr,com.apple.disk-image-cdr,dvdr,None
cel,public.flc-animation,flc,video/flc
cer,public.x509-certificate,cer,application/x-x509-ca-cert
cpp,public.c-plus-plus-source,cp,None
crt,public.x509-certificate,cer,application/x-x509-ca-cert
crw,com.canon.crw-raw-image,crw,image/x-canon-crw
cr2,com.canon.cr2-raw-image,cr2,None
cr3,com.canon.cr3-raw-image,cr3,None
csh,public.csh-script,csh,None
css,public.css,css,text/css
csv,public.comma-separated-values-text,csv,text/csv
cxx,public.c-plus-plus-source,cp,None
dae,org.khronos.collada.digital-asset-exchange,dae,None
dcm,org.nema.dicom,dcm,application/dicom
dcr,com.kodak.raw-image,dcr,None
dds,com.microsoft.dds,dds,None
der,public.x509-certificate,cer,application/x-x509-ca-cert
dif,public.dv-movie,dv,video/x-dv
dll,com.microsoft.windows-dynamic-link-library,dll,application/x-msdownload
dls,public.downloadable-sound,dls,audio/dls
dmg,com.apple.disk-image-udif,dmg,None
dng,com.adobe.raw-image,dng,image/x-adobe-dng
doc,com.microsoft.word.doc,doc,application/msword
dot,com.microsoft.word.dot,dot,application/msword
dxo,com.dxo.raw-image,dxo,image/x-dxo-dxo
ec3,public.enhanced-ac3-audio,eac3,audio/eac3
edn,com.adobe.edn,edn,None
efx,com.j2.efx-fax,efx,image/efax
eml,com.apple.mail.email,eml,message/rfc822
eps,com.adobe.encapsulated-postscript,eps,None
erf,com.epson.raw-image,erf,image/x-epson-erf
etd,com.adobe.etd,etd,None
exe,com.microsoft.windows-executable,exe,application/x-msdownload
exp,com.apple.symbol-export,exp,None
exr,com.ilm.openexr-image,exr,None
fdf,com.adobe.fdf,fdf,None
fff,com.hasselblad.fff-raw-image,fff,None
flc,public.flc-animation,flc,video/flc
fli,public.flc-animation,flc,video/flc
flv,com.adobe.flash.video,flv,video/x-flv
for,public.fortran-source,f,None
fpx,com.kodak.flashpix-image,fpx,image/fpx
f4a,com.adobe.flash.video,flv,video/x-flv
f4b,com.adobe.flash.video,flv,video/x-flv
f4p,com.adobe.flash.video,flv,video/x-flv
f4v,com.adobe.flash.video,flv,video/x-flv
f77,public.fortran-77-source,f77,None
f90,public.fortran-90-source,f90,None
f95,public.fortran-95-source,f95,None
gif,com.compuserve.gif,gif,image/gif
hdr,public.radiance,pic,None
hpp,public.c-plus-plus-header,hh,None
hqx,com.apple.binhex-archive,hqx,application/mac-binhex40
htm,public.html,html,text/html
hxx,public.c-plus-plus-header,hh,None
iba,com.apple.ibooksauthor.pkgbook,iba,None
icc,com.apple.colorsync-profile,icc,None
icm,com.apple.colorsync-profile,icc,None
ico,com.microsoft.ico,ico,image/vnd.microsoft.icon
ics,com.apple.ical.ics,ics,text/calendar
iig,com.apple.iig-source,iig,None
iiq,com.phaseone.raw-image,iiq,None
img,com.apple.disk-image-ndif,ndif,None
inl,public.c-plus-plus-inline-header,inl,None
ipa,com.apple.itunes.ipa,ipa,None
ipp,public.c-plus-plus-header,hh,None
ips,com.apple.ips,ips,None
iso,public.iso-image,iso,None
ite,com.apple.tv.ite,ite,None
itl,com.apple.itunes.db,itl,None
jar,com.sun.java-archive,jar,application/java-archive
jav,com.sun.java-source,java,None
jfx,com.j2.jfx-fax,jfx,None
jpe,public.jpeg,jpeg,image/jpeg
jpf,public.jpeg-2000,jp2,image/jp2
jpg,public.jpeg,jpeg,image/jpeg
jpx,public.jpeg-2000,jp2,image/jp2
jp2,public.jpeg-2000,jp2,image/jp2
j2c,public.jpeg-2000,jp2,image/jp2
j2k,public.jpeg-2000,jp2,image/jp2
kar,public.midi-audio,midi,audio/midi
key,com.apple.iwork.keynote.key,key,None
ksh,public.ksh-script,ksh,None
kth,com.apple.iwork.keynote.kth,kth,None
ktx,org.khronos.ktx,ktx,None
lid,public.dylan-source,dlyan,None
lmm,public.lex-source,l,None
log,com.apple.log,log,None
lpp,public.lex-source,l,None
lxx,public.lex-source,l,None
mid,public.midi-audio,midi,audio/midi
mig,public.mig-source,defs,None
mii,public.objective-c-plus-plus-source.preprocessed,mii,None
mjs,com.netscape.javascript-source,js,text/javascript
mnc,ca.mcgill.mni.bic.mnc,mnc,None
mos,com.leafamerica.raw-image,mos,None
mov,com.apple.quicktime-movie,mov,video/quicktime
mpe,public.mpeg,mpg,video/mpeg
mpg,public.mpeg,mpg,video/mpeg
mpo,public.mpo-image,mpo,None
mp2,public.mp2,mp2,None
mp3,public.mp3,mp3,audio/mpeg
mp4,public.mpeg-4,mp4,video/mp4
mrw,com.konicaminolta.raw-image,mrw,None
mts,public.avchd-mpeg-2-transport-stream,mts,None
mxf,org.smpte.mxf,mxf,application/mxf
m15,public.mpeg,mpg,video/mpeg
m2v,public.mpeg-2-video,m2v,video/mpeg2
m3u,public.m3u-playlist,m3u,audio/mpegurl
m4a,com.apple.m4a-audio,m4a,audio/x-m4a
m4b,com.apple.protected-mpeg-4-audio-b,m4b,None
m4p,com.apple.protected-mpeg-4-audio,m4p,None
m4r,com.apple.mpeg-4-ringtone,m4r,audio/x-m4r
m4v,com.apple.m4v-video,m4v,video/x-m4v
m75,public.mpeg,mpg,video/mpeg
nef,com.nikon.raw-image,nef,None
nii,gov.nih.nifti-1,nii,None
nrw,com.nikon.nrw-raw-image,nrw,image/x-nikon-nrw
obj,public.geometry-definition-format,obj,None
odb,org.oasis-open.opendocument.database,odb,application/vnd.oasis.opendocument.database
odc,org.oasis-open.opendocument.chart,odc,application/vnd.oasis.opendocument.chart
odf,org.oasis-open.opendocument.formula,odf,application/vnd.oasis.opendocument.formula
odg,org.oasis-open.opendocument.graphics,odg,application/vnd.oasis.opendocument.graphics
odi,org.oasis-open.opendocument.image,odi,application/vnd.oasis.opendocument.image
odm,org.oasis-open.opendocument.text-master,odm,application/vnd.oasis.opendocument.text-master
odp,org.oasis-open.opendocument.presentation,odp,application/vnd.oasis.opendocument.presentation
ods,org.oasis-open.opendocument.spreadsheet,ods,application/vnd.oasis.opendocument.spreadsheet
odt,org.oasis-open.opendocument.text,odt,application/vnd.oasis.opendocument.text
omf,com.avid.open-media-framework,omf,None
orf,com.olympus.raw-image,orf,None
otc,public.opentype-collection-font,otc,None
otf,public.opentype-font,otf,None
otg,org.oasis-open.opendocument.graphics-template,otg,application/vnd.oasis.opendocument.graphics-template
oth,org.oasis-open.opendocument.text-web,oth,application/vnd.oasis.opendocument.text-web
oti,org.oasis-open.opendocument.image-template,oti,application/vnd.oasis.opendocument.image-template
otp,org.oasis-open.opendocument.presentation-template,otp,application/vnd.oasis.opendocument.presentation-template
ots,org.oasis-open.opendocument.spreadsheet-template,ots,application/vnd.oasis.opendocument.spreadsheet-template
ott,org.oasis-open.opendocument.text-template,ott,application/vnd.oasis.opendocument.text-template
pas,public.pascal-source,pas,None
pax,public.cpio-archive,cpio,None
pbm,public.pbm,pbm,None
pch,public.precompiled-c-header,pch,None
pct,com.apple.pict,pict,image/pict
pdf,com.adobe.pdf,pdf,application/pdf
pef,com.pentax.raw-image,pef,None
pem,public.x509-certificate,cer,application/x-x509-ca-cert
pfa,com.adobe.postscript-pfa-font,pfa,None
pfb,com.adobe.postscript-pfb-font,pfb,None
pfm,public.pbm,pbm,None
pfx,com.rsa.pkcs-12,p12,application/x-pkcs12
pgm,public.pbm,pbm,None
pgn,com.apple.chess.pgn,pgn,None
php,public.php-script,php,text/php
ph3,public.php-script,php,text/php
ph4,public.php-script,php,text/php
pic,com.apple.pict,pict,image/pict
pkg,com.apple.installer-package-archive,pkg,None
pls,public.pls-playlist,pls,audio/x-scpls
ply,public.polygon-file-format,ply,None
png,public.png,png,image/png
pot,com.microsoft.powerpoint.pot,pot,application/vnd.ms-powerpoint
ppm,public.pbm,pbm,None
pps,com.microsoft.powerpoint.pps,pps,application/vnd.ms-powerpoint
ppt,com.microsoft.powerpoint.ppt,ppt,application/vnd.ms-powerpoint
psb,com.adobe.photoshop-large-image,psb,None
psd,com.adobe.photoshop-image,psd,image/vnd.adobe.photoshop
pvr,public.pvr,pvr,None
pvt,com.apple.private.live-photo-bundle,pvt,None
pwl,com.leica.pwl-raw-image,pwl,image/x-leica-pwl
p12,com.rsa.pkcs-12,p12,application/x-pkcs12
qti,com.apple.quicktime-image,qtif,image/x-quicktime
qtz,com.apple.quartz-composer-composition,qtz,application/x-quartzcomposer
raf,com.fuji.raw-image,raf,None
ram,com.real.realaudio,ram,audio/vnd.rn-realaudio
raw,com.panasonic.raw-image,raw,None
rbw,public.ruby-script,rb,text/x-ruby-script
rmp,com.apple.music.rmp-playlist,rmp,application/vnd.rn-rn_music_package
rss,public.rss,rss,application/rss+xml
rtf,public.rtf,rtf,text/rtf
rwl,com.leica.rwl-raw-image,rwl,None
rw2,com.panasonic.rw2-raw-image,rw2,None
scc,com.scenarist.closed-caption,scc,None
scn,com.apple.scenekit.scene,scn,None
sda,org.openoffice.graphics,sxd,application/vnd.sun.xml.draw
sdc,org.openoffice.spreadsheet,sxc,application/vnd.sun.xml.calc
sdd,org.openoffice.presentation,sxi,application/vnd.sun.xml.impress
sdp,org.openoffice.presentation,sxi,application/vnd.sun.xml.impress
sdv,public.3gpp,3gp,video/3gpp
sdw,org.openoffice.text,sxw,application/vnd.sun.xml.writer
sd2,com.digidesign.sd2-audio,sd2,None
sea,com.stuffit.archive.sit,sit,application/x-stuffit
sf2,com.soundblaster.soundfont,sf2,None
sgi,com.sgi.sgi-image,sgi,image/sgi
sit,com.stuffit.archive.sit,sit,application/x-stuffit
slm,com.apple.photos.slow-motion-video-sidecar,slm,None
smf,public.midi-audio,midi,audio/midi
smi,com.apple.disk-image-smi,smi,None
snd,public.au-audio,au,audio/basic
spx,com.apple.systemprofiler.document,spx,None
srf,com.sony.raw-image,srf,None
srw,com.samsung.raw-image,srw,None
sr2,com.sony.sr2-raw-image,sr2,image/x-sony-sr2
stc,org.openoffice.spreadsheet-template,stc,application/vnd.sun.xml.calc.template
std,org.openoffice.graphics-template,std,application/vnd.sun.xml.draw.template
sti,org.openoffice.presentation-template,sti,application/vnd.sun.xml.impress.template
stl,public.standard-tesselated-geometry-format,stl,None
stw,org.openoffice.text-template,stw,application/vnd.sun.xml.writer.template
svg,public.svg-image,svg,image/svg+xml
sxc,org.openoffice.spreadsheet,sxc,application/vnd.sun.xml.calc
sxd,org.openoffice.graphics,sxd,application/vnd.sun.xml.draw
sxg,org.openoffice.text-master,sxg,application/vnd.sun.xml.writer.global
sxi,org.openoffice.presentation,sxi,application/vnd.sun.xml.impress
sxm,org.openoffice.formula,sxm,application/vnd.sun.xml.math
sxw,org.openoffice.text,sxw,application/vnd.sun.xml.writer
tar,public.tar-archive,tar,application/x-tar
tbz,public.tar-bzip2-archive,tbz2,None
tga,com.truevision.tga-image,tga,image/targa
tgz,org.gnu.gnu-zip-tar-archive,tgz,None
tif,public.tiff,tiff,image/tiff
tsv,public.tab-separated-values-text,tsv,text/tab-separated-values
ttc,public.truetype-collection-font,ttc,None
ttf,public.truetype-ttf-font,ttf,None
txt,public.plain-text,txt,text/plain
ulw,public.ulaw-audio,ul,None
url,com.microsoft.internet-shortcut,url,None
usd,com.pixar.universal-scene-description,usd,None
vcf,public.vcard,vcf,text/directory
vcs,com.apple.ical.vcs,vcs,text/x-vcalendar
vfw,public.avi,avi,video/avi
vtt,org.w3.webvtt,vtt,text/vtt
war,com.sun.web-application-archive,war,None
wav,com.microsoft.waveform-audio,wav,audio/vnd.wave
wax,com.microsoft.windows-media-wax,wax,video/x-ms-wax
web,com.getdropbox.dropbox.shortcut,web,None
wma,com.microsoft.windows-media-wma,wma,video/x-ms-wma
wmp,com.microsoft.windows-media-wmp,wmp,video/x-ms-wmp
wmv,com.microsoft.windows-media-wmv,wmv,video/x-ms-wmv
wmx,com.microsoft.windows-media-wmx,wmx,video/x-ms-wmx
wvx,com.microsoft.windows-media-wvx,wvx,video/x-ms-wvx
xar,com.apple.xar-archive,xar,None
xbm,public.xbitmap-image,xbm,image/x-xbitmap
xfd,public.xfd,xfd,None
xht,public.xhtml,xhtml,application/xhtml+xml
xip,com.apple.xip-archive,xip,None
xla,com.microsoft.excel.xla,xla,None
xls,com.microsoft.excel.xls,xls,application/vnd.ms-excel
xlt,com.microsoft.excel.xlt,xlt,application/vnd.ms-excel
xlw,com.microsoft.excel.xlw,xlw,application/vnd.ms-excel
xml,public.xml,xml,application/xml
xmp,com.seriflabs.xmp,xmp,application/rdf+xml
xpc,com.apple.xpc-service,xpc,None
yml,public.yaml,yml,application/x-yaml
ymm,public.yacc-source,y,None
ypp,public.yacc-source,y,None
yxx,public.yacc-source,y,None
zip,public.zip-archive,zip,application/zip
zsh,public.zsh-script,zsh,None
3fr,com.hasselblad.3fr-raw-image,3fr,None
3gp,public.3gpp,3gp,video/3gpp
3g2,public.3gpp2,3g2,video/3gpp2
adts,public.aac-audio,aac,audio/aac
ahap,com.apple.haptics.ahap,ahap,None
aifc,public.aifc-audio,aifc,audio/aiff
aiff,public.aifc-audio,aifc,audio/aiff
astc,org.khronos.astc,astc,None
avci,public.avci,avci,image/avci
avcs,public.avcs,avcs,image/avcs
band,com.apple.garageband.project,band,None
bash,public.bash-script,bash,None
bdmv,public.avchd-content,bdm,None
book,com.apple.ibooksauthor.pkgbook,iba,None
cdda,public.aifc-audio,aifc,audio/aiff
chat,com.apple.ichat.transcript,ichat,None
cpgz,com.apple.bom-compressed-cpio,cpgz,None
cpio,public.cpio-archive,cpio,None
dart,com.apple.disk-image-dart,dart,None
dc42,com.apple.disk-image-dc42,dc42,None
defs,public.mig-source,defs,None
dext,com.apple.driver-extension,dext,None
diff,public.patch-file,patch,None
dist,com.apple.installer-distribution-package,dist,None
docm,org.openxmlformats.wordprocessingml.document.macroenabled,docm,application/vnd.ms-word.document.macroenabled.12
docx,org.openxmlformats.wordprocessingml.document,docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document
dotm,org.openxmlformats.wordprocessingml.template.macroenabled,dotm,application/vnd.ms-word.template.macroenabled.12
dotx,org.openxmlformats.wordprocessingml.template,dotx,application/vnd.openxmlformats-officedocument.wordprocessingml.template
dsym,com.apple.xcode.dsym,dsym,None
dvdr,com.apple.disk-image-cdr,dvdr,None
eac3,public.enhanced-ac3-audio,eac3,audio/eac3
emlx,com.apple.mail.emlx,emlx,None
enex,com.evernote.enex,enex,None
epub,org.idpf.epub-container,epub,application/epub+zip
fh10,com.seriflabs.affinity,fh10,None
fh11,com.seriflabs.affinity,fh10,None
flac,org.xiph.flac,flac,audio/flac
fpbf,com.apple.finder.burn-folder,fpbf,None
game,com.apple.chess.game,game,None
gdoc,com.google.gdoc,gdoc,None
gtar,org.gnu.gnu-tar-archive,gtar,application/x-gtar
gzip,org.gnu.gnu-zip-archive,gz,application/x-gzip
hang,com.apple.hangreport,hang,None
heic,public.heic,heic,image/heic
heif,public.heif,heif,image/heif
html,public.html,html,text/html
hvpl,com.apple.music.visual,hvpl,None
icbu,com.apple.ical.backup,icbu,None
icns,com.apple.icns,icns,None
ipsw,com.apple.itunes.ipsw,ipsw,None
itlp,com.apple.music.itlp,itlp,None
itms,com.apple.itunes.store-url,itms,None
java,com.sun.java-source,java,None
jnlp,com.sun.java-web-start,jnlp,application/x-java-jnlp-file
jpeg,public.jpeg,jpeg,image/jpeg
json,public.json,json,application/json
latm,public.mp4a-loas,loas,None
loas,public.mp4a-loas,loas,None
lpdf,com.apple.localized-pdf-bundle,lpdf,None
mbox,com.apple.mail.mbox,mbox,None
menu,com.apple.menu-extra,menu,None
midi,public.midi-audio,midi,audio/midi
minc,ca.mcgill.mni.bic.mnc,mnc,None
mpeg,public.mpeg,mpg,video/mpeg
mpga,public.mp3,mp3,audio/mpeg
mpg4,public.mpeg-4,mp4,video/mp4
mpkg,com.apple.installer-package-archive,pkg,None
m2ts,public.avchd-mpeg-2-transport-stream,mts,None
m3u8,public.m3u-playlist,m3u,audio/mpegurl
ndif,com.apple.disk-image-ndif,ndif,None
note,com.apple.notes.note,note,None
php3,public.php-script,php,text/php
php4,public.php-script,php,text/php
pict,com.apple.pict,pict,image/pict
pntg,com.apple.macpaint-image,pntg,None
potm,org.openxmlformats.presentationml.template.macroenabled,potm,application/vnd.ms-powerpoint.template.macroenabled.12
potx,org.openxmlformats.presentationml.template,potx,application/vnd.openxmlformats-officedocument.presentationml.template
ppsm,org.openxmlformats.presentationml.slideshow.macroenabled,ppsm,application/vnd.ms-powerpoint.slideshow.macroenabled.12
ppsx,org.openxmlformats.presentationml.slideshow,ppsx,application/vnd.openxmlformats-officedocument.presentationml.slideshow
pptm,org.openxmlformats.presentationml.presentation.macroenabled,pptm,application/vnd.ms-powerpoint.presentation.macroenabled.12
pptx,org.openxmlformats.presentationml.presentation,pptx,application/vnd.openxmlformats-officedocument.presentationml.presentation
pset,com.apple.pdf-printer-settings,pset,None
qtif,com.apple.quicktime-image,qtif,image/x-quicktime
rmvb,com.real.realmedia-vbr,rmvb,application/vnd.rn-realmedia-vbr
rtfd,com.apple.rtfd,rtfd,None
scnz,com.apple.scenekit.scene,scn,None
scpt,com.apple.applescript.script,scpt,None
shtm,public.html,html,text/html
sidx,com.stuffit.archive.sidx,sidx,application/x-stuffitx-index
sitx,com.stuffit.archive.sitx,sitx,application/x-stuffitx
spin,com.apple.spinreport,spin,None
suit,com.apple.font-suitcase,suit,None
svgz,public.svg-image,svg,image/svg+xml
tbz2,public.tar-bzip2-archive,tbz2,None
tcsh,public.tcsh-script,tcsh,None
term,com.apple.terminal.session,term,None
text,public.plain-text,txt,text/plain
tiff,public.tiff,tiff,image/tiff
tool,com.apple.terminal.shell-script,command,None
udif,com.apple.disk-image-udif,dmg,None
ulaw,public.ulaw-audio,ul,None
usda,com.pixar.universal-scene-description,usd,None
usdc,com.pixar.universal-scene-description,usd,None
usdz,com.pixar.universal-scene-description-mobile,usdz,model/vnd.usdz+zip
vcal,com.apple.ical.vcs,vcs,text/x-vcalendar
wave,com.microsoft.waveform-audio,wav,audio/vnd.wave
wdgt,com.apple.dashboard-widget,wdgt,None
webp,public.webp,webp,None
xfdf,com.adobe.xfdf,xfdf,None
xhtm,public.xhtml,xhtml,application/xhtml+xml
xlsb,com.microsoft.excel.sheet.binary.macroenabled,xlsb,application/vnd.ms-excel.sheet.binary.macroenabled.12
xlsm,org.openxmlformats.spreadsheetml.sheet.macroenabled,xlsm,application/vnd.ms-excel.sheet.macroenabled.12
xlsx,org.openxmlformats.spreadsheetml.sheet,xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
xltm,org.openxmlformats.spreadsheetml.template.macroenabled,xltm,application/vnd.ms-excel.template.macroenabled.12
xltx,org.openxmlformats.spreadsheetml.template,xltx,application/vnd.openxmlformats-officedocument.spreadsheetml.template
yaml,public.yaml,yml,application/x-yaml
3gpp,public.3gpp,3gp,video/3gpp
3gp2,public.3gpp2,3g2,video/3gpp2
abcdg,com.apple.addressbook.group,abcdg,None
abcdp,com.apple.addressbook.person,abcdp,None
afpub,com.seriflabs.affinitypublisher.document,afpub,None
appex,com.apple.application-and-system-extension,appex,None
avchd,public.avchd-collection,avchd,None
blank,com.apple.preview.blank,blank,None
class,com.sun.java-class,class,None
crash,com.apple.crashreport,crash,None
dfont,com.apple.truetype-datafork-suitcase-font,dfont,None
dicom,org.nema.dicom,dcm,application/dicom
distz,com.apple.installer-distribution-package,dist,None
dlyan,public.dylan-source,dlyan,None
dylib,com.apple.mach-o-dylib,dylib,None
heics,public.heics,heics,image/heic-sequence
heifs,public.heifs,heifs,image/heif-sequence
ichat,com.apple.ichat.transcript,ichat,None
pages,com.apple.iwork.pages.pages,pages,None
panic,com.apple.panicreport,panic,None
paper,com.getdropbox.dropbox.paper,paper,None
patch,public.patch-file,patch,None
phtml,public.php-script,php,text/php
plist,com.apple.property-list,plist,None
saver,com.apple.systempreference.screen-saver,saver,None
scptd,com.apple.applescript.script-bundle,scptd,None
sfont,com.apple.cfr-font,sfont,None
shtml,public.html,html,text/html
swift,public.swift-source,swift,None
toast,com.roxio.disk-image-toast,toast,None
vcard,public.vcard,vcf,text/directory
wdmon,com.apple.wireless-diagnostics.wdmon,wdmon,None
xhtml,public.xhtml,xhtml,application/xhtml+xml
action,com.apple.automator-action,action,None
afploc,com.apple.afp-internet-location,afploc,None
"""
# load CSV separated uti data into dictionaries with key of extension and UTI
EXT_UTI_DICT = {}
UTI_EXT_DICT = {}
def _load_uti_dict():
"""load an initialize the cached UTI and extension dicts"""
_reader = csv.DictReader(UTI_CSV.split("\n"), delimiter=",")
for row in _reader:
EXT_UTI_DICT[row["extension"]] = row["UTI"]
UTI_EXT_DICT[row["UTI"]] = row["preferred_extension"]
_load_uti_dict()
# OS version for determining which methods can be used
OS_VER, OS_MAJOR, _ = (int(x) for x in _get_os_version())
def _get_uti_from_mdls(extension):
"""use mdls to get the UTI for a given extension on systems that don't support UTTypeCreatePreferredIdentifierForTag
Returns: UTI or None if UTI cannot be determined"""
# mdls -name kMDItemContentType foo.3fr
# kMDItemContentType = "com.hasselblad.3fr-raw-image"
with tempfile.NamedTemporaryFile(suffix="." + extension) as temp:
output = subprocess.check_output(
[
"/usr/bin/mdls",
"-name",
"kMDItemContentType",
temp.name,
]
).splitlines()
output = output[0].decode("UTF-8") if output else None
if not output:
return None
match = re.match(r'kMDItemContentType\s+\=\s+"(.*)"', output)
if match:
return match.group(1)
return None
def _get_uti_from_ext_dict(ext):
try:
return EXT_UTI_DICT[ext]
except KeyError:
return None
def _get_ext_from_uti_dict(uti):
try:
return UTI_EXT_DICT[uti]
except KeyError:
return None
def get_preferred_uti_extension(uti):
"""get preferred extension for a UTI type
uti: UTI str, e.g. 'public.jpeg'
returns: preferred extension as str or None if cannot be determined"""
if (OS_VER, OS_MAJOR) <= (10, 16):
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
# deprecated in Catalina+, likely won't work at all on macOS 12
with objc.autorelease_pool():
extension = CoreServices.UTTypeCopyPreferredTagWithClass(
uti, CoreServices.kUTTagClassFilenameExtension
)
if extension:
return extension
# on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC
if uti == "public.heic":
return "HEIC"
return None
return _get_ext_from_uti_dict(uti)
def get_uti_for_extension(extension):
"""get UTI for a given file extension"""
# accepts extension with or without leading 0
if extension[0] == ".":
extension = extension[1:]
if (OS_VER, OS_MAJOR) <= (10, 16):
# https://developer.apple.com/documentation/coreservices/1448939-uttypecreatepreferredidentifierf
with objc.autorelease_pool():
uti = CoreServices.UTTypeCreatePreferredIdentifierForTag(
CoreServices.kUTTagClassFilenameExtension, extension, None
)
if uti:
return uti
# on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC
if extension.lower() == "heic":
return "public.heic"
return None
uti = _get_uti_from_ext_dict(extension)
if uti:
return uti
uti = _get_uti_from_mdls(extension)
if uti:
# cache the UTI
EXT_UTI_DICT[extension.lower()] = uti
UTI_EXT_DICT[uti] = extension.lower()
return uti
return None

View File

@@ -19,8 +19,6 @@ from plistlib import load as plistload
from typing import Callable
import CoreFoundation
import CoreServices
import objc
from ._constants import UNICODE_FORMAT
@@ -38,7 +36,7 @@ if not _DEBUG:
def _get_logger():
"""Used only for testing
Returns:
logging.Logger object -- logging.Logger object for osxphotos
"""
@@ -46,7 +44,7 @@ def _get_logger():
def _set_debug(debug):
""" Enable or disable debug logging """
"""Enable or disable debug logging"""
global _DEBUG
_DEBUG = debug
if debug:
@@ -56,18 +54,18 @@ def _set_debug(debug):
def _debug():
""" returns True if debugging turned on (via _set_debug), otherwise, false """
"""returns True if debugging turned on (via _set_debug), otherwise, false"""
return _DEBUG
def noop(*args, **kwargs):
""" do nothing (no operation) """
"""do nothing (no operation)"""
pass
def lineno(filename):
""" Returns string with filename and current line number in caller as '(filename): line_num'
Will trim filename to just the name, dropping path, if any. """
"""Returns string with filename and current line number in caller as '(filename): line_num'
Will trim filename to just the name, dropping path, if any."""
line = inspect.currentframe().f_back.f_lineno
filename = pathlib.Path(filename).name
return f"{filename}: {line}"
@@ -92,14 +90,14 @@ def _get_os_version():
def _check_file_exists(filename):
""" returns true if file exists and is not a directory
otherwise returns false """
"""returns true if file exists and is not a directory
otherwise returns false"""
filename = os.path.abspath(filename)
return os.path.exists(filename) and not os.path.isdir(filename)
def _get_resource_loc(model_id):
""" returns folder_id and file_id needed to find location of edited photo """
"""returns folder_id and file_id needed to find location of edited photo"""
""" and live photos for version <= Photos 4.0 """
# determine folder where Photos stores edited version
# edited images are stored in:
@@ -117,7 +115,7 @@ def _get_resource_loc(model_id):
def _dd_to_dms(dd):
""" convert lat or lon in decimal degrees (dd) to degrees, minutes, seconds """
"""convert lat or lon in decimal degrees (dd) to degrees, minutes, seconds"""
""" return tuple of int(deg), int(min), float(sec) """
dd = float(dd)
negative = dd < 0
@@ -136,7 +134,7 @@ def _dd_to_dms(dd):
def dd_to_dms_str(lat, lon):
""" convert latitude, longitude in degrees to degrees, minutes, seconds as string """
"""convert latitude, longitude in degrees to degrees, minutes, seconds as string"""
""" lat: latitude in degrees """
""" lon: longitude in degrees """
""" returns: string tuple in format ("51 deg 30' 12.86\" N", "0 deg 7' 54.50\" W") """
@@ -165,7 +163,7 @@ def dd_to_dms_str(lat, lon):
def get_system_library_path():
""" return the path to the system Photos library as string """
"""return the path to the system Photos library as string"""
""" only works on MacOS 10.15 """
""" on earlier versions, returns None """
_, major, _ = _get_os_version()
@@ -190,8 +188,8 @@ def get_system_library_path():
def get_last_library_path():
""" returns the path to the last opened Photos library
If a library has never been opened, returns None """
"""returns the path to the last opened Photos library
If a library has never been opened, returns None"""
plist_file = pathlib.Path(
str(pathlib.Path.home())
+ "/Library/Containers/com.apple.Photos/Data/Library/Preferences/com.apple.Photos.plist"
@@ -241,7 +239,7 @@ def get_last_library_path():
def list_photo_libraries():
""" returns list of Photos libraries found on the system """
"""returns list of Photos libraries found on the system"""
""" on MacOS < 10.15, this may omit some libraries """
# On 10.15, mdfind appears to find all libraries
@@ -265,31 +263,10 @@ def list_photo_libraries():
return lib_list
def get_preferred_uti_extension(uti):
""" get preferred extension for a UTI type
uti: UTI str, e.g. 'public.jpeg'
returns: preferred extension as str or None if cannot be determined """
# reference: https://developer.apple.com/documentation/coreservices/1442744-uttypecopypreferredtagwithclass?language=objc
with objc.autorelease_pool():
extension = CoreServices.UTTypeCopyPreferredTagWithClass(
uti, CoreServices.kUTTagClassFilenameExtension
)
if extension:
return extension
# on MacOS 10.12, HEIC files are not supported and UTTypeCopyPreferredTagWithClass will return None for HEIC
if uti == "public.heic":
return "HEIC"
return None
def findfiles(pattern, path_):
"""Returns list of filenames from path_ matched by pattern
shell pattern. Matching is case-insensitive.
If 'path_' is invalid/doesn't exist, returns []."""
shell pattern. Matching is case-insensitive.
If 'path_' is invalid/doesn't exist, returns []."""
if not os.path.isdir(path_):
return []
# See: https://gist.github.com/techtonik/5694830
@@ -316,8 +293,8 @@ def findfiles(pattern, path_):
def _open_sql_file(dbname):
""" opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor) """
"""opens sqlite file dbname in read-only mode
returns tuple of (connection, cursor)"""
try:
dbpath = pathlib.Path(dbname).resolve()
conn = sqlite3.connect(f"{dbpath.as_uri()}?mode=ro", timeout=1, uri=True)
@@ -328,9 +305,9 @@ def _open_sql_file(dbname):
def _db_is_locked(dbname):
""" check to see if a sqlite3 db is locked
returns True if database is locked, otherwise False
dbname: name of database to test """
"""check to see if a sqlite3 db is locked
returns True if database is locked, otherwise False
dbname: name of database to test"""
# first, check to see if lock file exists, if so, assume the file is locked
lock_name = f"{dbname}.lock"
@@ -381,7 +358,7 @@ def _db_is_locked(dbname):
def normalize_unicode(value):
""" normalize unicode data """
"""normalize unicode data"""
if value is not None:
if isinstance(value, (tuple, list)):
return tuple(unicodedata.normalize(UNICODE_FORMAT, v) for v in value)
@@ -394,9 +371,9 @@ def normalize_unicode(value):
def increment_filename(filepath):
""" Return filename (1).ext, etc if filename.ext exists
"""Return filename (1).ext, etc if filename.ext exists
If file exists in filename's parent folder with same stem as filename,
If file exists in filename's parent folder with same stem as filename,
add (1), (2), etc. until a non-existing filename is found.
Args:
@@ -419,8 +396,22 @@ def increment_filename(filepath):
return str(dest)
def expand_and_validate_filepath(path: str) -> str:
"""validate and expand ~ in filepath, also un-escapes spaces
Returns:
expanded path if path is valid file, else None
"""
path = re.sub(r"\\ ", " ", path)
path = pathlib.Path(path).expanduser()
if path.is_file():
return str(path)
return None
def load_function(pyfile: str, function_name: str) -> Callable:
""" Load function_name from python file pyfile """
"""Load function_name from python file pyfile"""
module_file = pathlib.Path(pyfile)
if not module_file.is_file():
raise FileNotFoundError(f"module {pyfile} does not appear to exist")

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LibrarySchemaVersion</key>
<integer>5001</integer>
<key>MetaSchemaVersion</key>
<integer>3</integer>
</dict>
</plist>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>hostname</key>
<string>ms-MacBook-Pro.local</string>
<key>hostuuid</key>
<string>793FB248-A75B-5F63-9A2E-76E4BFA8E877</string>
<key>pid</key>
<integer>391</integer>
<key>processname</key>
<string>photolibraryd</string>
<key>uid</key>
<integer>501</integer>
</dict>
</plist>

Binary file not shown.

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>insertAlbum</key>
<array/>
<key>insertAsset</key>
<array/>
<key>insertHighlight</key>
<array/>
<key>insertMemory</key>
<array/>
<key>insertMoment</key>
<array/>
<key>removeAlbum</key>
<array/>
<key>removeAsset</key>
<array/>
<key>removeHighlight</key>
<array/>
<key>removeMemory</key>
<array/>
<key>removeMoment</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>embeddingVersion</key>
<string>1</string>
<key>localeIdentifier</key>
<string>en_US</string>
<key>sceneTaxonomySHA</key>
<string>dd7ff94fd9919a493393b86581994e1db06a3553f80052c5e8343c0443848344</string>
<key>searchIndexVersion</key>
<string>15065</string>
</dict>
</plist>

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 500 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 524 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 KiB

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>MigrationService</key>
<dict>
<key>State</key>
<integer>4</integer>
</dict>
<key>MigrationService.LastCompletedTask</key>
<integer>12</integer>
<key>MigrationService.ValidationCounts</key>
<dict>
<key>MigrationDetectedFaceprint</key>
<integer>6</integer>
<key>MigrationManagedAsset</key>
<integer>0</integer>
<key>MigrationSceneClassification</key>
<integer>44</integer>
<key>MigrationUnmanagedAdjustment</key>
<integer>0</integer>
<key>RDVersion.cloudLocalState.CPLIsNotPushed</key>
<integer>7</integer>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>92D68107-B6C7-453B-96D2-97B0F26D5B8B/L0/020</string>
<string>88A5F8B8-5B9A-43C7-BB85-3952B81580EB/L0/020</string>
<string>29EF7A97-7E76-4D5F-A5E0-CC0A93E8524C/L0/020</string>
<string>2C2AF115-BD1D-4434-A747-D1C8BD8E2045/L0/020</string>
<string>CB051A4C-2CB7-4B90-B59B-08CC4D0C2823/L0/020</string>
</array>
<key>Photos</key>
<dict>
<key>CollapsedSidebarSectionIdentifiers</key>
<array/>
<key>ExpandedSidebarItemIdentifiers</key>
<array>
<string>TopLevelAlbums</string>
<string>TopLevelSlideshows</string>
</array>
<key>IPXWorkspaceControllerZoomLevelsKey</key>
<dict>
<key>kZoomLevelIdentifierAlbums</key>
<integer>7</integer>
<key>kZoomLevelIdentifierVersions</key>
<integer>7</integer>
</dict>
<key>lastAddToDestination</key>
<dict>
<key>key</key>
<integer>1</integer>
<key>lastKnownDisplayName</key>
<string>September 28, 2018</string>
<key>type</key>
<string>album</string>
<key>uuid</key>
<string>DFFKmHt3Tk+AGzZLe2Xq+g</string>
</dict>
<key>lastKnownItemCounts</key>
<dict>
<key>other</key>
<integer>0</integer>
<key>photos</key>
<integer>7</integer>
<key>videos</key>
<integer>0</integer>
</dict>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BackgroundHighlightCollection</key>
<date>2021-03-13T16:38:25Z</date>
<key>BackgroundHighlightEnrichment</key>
<date>2021-03-13T16:38:24Z</date>
<key>BackgroundJobAssetRevGeocode</key>
<date>2021-03-13T16:38:25Z</date>
<key>BackgroundJobSearch</key>
<date>2021-03-13T16:38:25Z</date>
<key>BackgroundPeopleSuggestion</key>
<date>2021-03-13T16:38:23Z</date>
<key>BackgroundUserBehaviorProcessor</key>
<date>2021-03-13T16:38:25Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2020-10-17T23:45:33Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2020-10-17T23:45:24Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2021-03-13T16:38:25Z</date>
<key>SiriPortraitDonation</key>
<date>2021-03-13T16:38:25Z</date>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>revgeoprovider</key>
<string>7618</string>
</dict>
</plist>

Some files were not shown because too many files have changed in this diff Show More