diff --git a/README.md b/README.md index 34fc25b7c..d17d2eb54 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Limited support is also provided for exporting photos and metadata from iPhoto l This package will read Photos databases for any supported version on any supported macOS version. E.g. you can read a database created with Photos 5.0 on MacOS 10.15 on a machine running macOS 10.12 and vice versa. -Requires python >= `3.9`, <= `3.12`. Reading iPhoto libraries requires python >= `3.10`. +Requires python >= `3.10`, <= `3.13`. For macOS 15.0 / Sequoia developer preview, alpha support is provided (very preliminary, not guaranteed to work). Not all features of osxphotos have been tested and some features may not work. If you encounter issues, please open an issue on GitHub. @@ -357,7 +357,7 @@ If your photos are organized in folders and albums in Photos you can preserve th `osxphotos export /path/to/export --directory "{folder_album}"` -Photos can belong to more than one album. In this case, the template field `{folder_album}` will expand to all the album names that the photo belongs to. For example, if a photo belongs to the albums `Vacation` and `Travel`, the template field `{folder_album}` would expand to `Vacation`, `Travel`. If the photo belongs to no albums, the template field `{folder_album}` would expand to "_" (the default value). +Photos can belong to more than one album. In this case, the template field `{folder_album}` will expand to all the album names that the photo belongs to. For example, if a photo belongs to the albums `Vacation` and `Travel`, the template field `{folder_album}` would expand to `Vacation`, `Travel`. If the photo belongs to no albums, the template field `{folder_album}` would expand to "_" (the default value). All template fields including `{folder_album}` can be further filtered using a number of different filters. To convert all directory names to lower case for example, use the `lower` filter: @@ -378,8 +378,8 @@ By default, osxphotos will use the original filename of the photo when exporting The above command will export photos using the title. Note that you don't need to specify the extension as part of the `--filename` template as osxphotos will automatically add the correct file extension. Some photos might not have a title so in this case, you could use the default value feature to specify a different name for these photos. For example, to use the title as the filename, but if no title is specified, use the original filename instead: osxphotos export /path/to/export --filename "{title,{original_name}}" - │ ││ │ - │ ││ │ + │ ││ │ + │ ││ │ Use photo's title as the filename <──────┘ ││ │ ││ │ Value after comma will be used <───────┘│ │ @@ -393,8 +393,8 @@ The above command will export photos using the title. Note that you don't need The osxphotos template system also allows for limited conditional logic of the type "If a condition is true then do one thing, otherwise, do a different thing". For example, you can use the `--filename` option to name files that are marked as "Favorites" in Photos differently than other files. For example, to add a "#" to the name of every photo that's a favorite: osxphotos export /path/to/export --filename "{original_name}{favorite?#,}" - │ │ │││ - │ │ │││ + │ │ │││ + │ │ │││ Use photo's original name as filename <──┘ │ │││ │ │││ 'favorite' is True if photo is a Favorite, <───────┘ │││ @@ -450,7 +450,7 @@ All the above commands operate on the default Photos library. Most users only u #### Missing photos -osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac. +osxphotos works by copying photos out of the Photos library folder to export them. You may see osxphotos report that one or more photos are missing and thus could not be exported. One possible reason for this is that you are using iCloud to synch your Photos library and Photos either hasn't yet synched the cloud library to the local Mac or you have Photos configured to "Optimize Mac Storage" in Photos Preferences. Another reason is that even if you have Photos configured to download originals to the Mac, Photos does not always download photos from shared albums or original screenshots to the Mac. If you encounter missing photos you can tell osxphotos to download the missing photos from iCloud using the `--download-missing` option. `--download-missing` uses AppleScript to communicate with Photos and tell it to download the missing photos. Photos' AppleScript interface is somewhat buggy and you may find that Photos crashes. In this case, osxphotos will attempt to restart Photos to resume the download process. There's also an experimental `--use-photokit` option that will communicate with Photos using a different "PhotoKit" interface. This option must be used together with `--download-missing`: @@ -480,12 +480,12 @@ This will write basic metadata such as keywords, persons, and GPS location to th osxphotos export /path/to/export --exiftool --keyword-template "{folder_album(>)}" │ │ - │ │ - folder_album results in the folder(s) <──┘ │ - and album a photo is contained in │ - │ - The value in () is used as the path separator <───────┘ - for joining the folders and albums. For example, + │ │ + folder_album results in the folder(s) <──┘ │ + and album a photo is contained in │ + │ + The value in () is used as the path separator <───────┘ + for joining the folders and albums. For example, if photo is in Folder1/Folder2/Album, (>) produces "Folder1>Folder2>Album" which some programs, such as Lightroom Classic, treat as hierarchical keywords @@ -514,7 +514,7 @@ Another way to export metadata about your photos is through the use of sidecar f `osxphotos export /path/to/export --sidecar XMP` -Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`. +Unlike `--exiftool`, you do not need to install exiftool to use the `--sidecar` feature. Many of the same configuration options that apply to `--exiftool` to modify metadata, for example, `--keyword-template` can also be used with `--sidecar`. Sidecar files are named "photoname.ext.sidecar_ext". For example, if the photo is named `IMG_1234.JPG` and the sidecar format is XMP, the sidecar would be named `IMG_1234.JPG.XMP`. Some applications expect the sidecar in this case to be named `IMG_1234.XMP`. You can use the `--sidecar-drop-ext` option to force osxphotos to name the sidecar files in this manner: @@ -621,21 +621,21 @@ In the template string above, `{newline}` instructs osxphotos to insert a new li Explanation of the template string: {title,}{title?{descr?{newline},},}{descr,} - │ │ │ │ │ │ │ - │ │ │ │ │ │ │ - └──> insert title (or nothing if no title) + │ │ │ │ │ │ │ + │ │ │ │ │ │ │ + └──> insert title (or nothing if no title) │ │ │ │ │ │ └───> is there a title? │ │ │ │ │ - └───> if so, is there a description? + └───> if so, is there a description? │ │ │ │ - └───> if so, insert new line + └───> if so, insert new line │ │ │ └───> if descr is blank, insert nothing - │ │ + │ │ └───> if title is blank, insert nothing │ - └───> finally, insert description + └───> finally, insert description (or nothing if no description) In this example, `title?` demonstrates use of the boolean (True/False) feature of the template system. `title?` is read as "Is the title True (or not blank/empty)? If so, then the value immediately following the `?` is used in place of `title`. If `title` is blank, then the value immediately following the comma is used instead. The format for boolean fields is `field?value if true,value if false`. Either `value if true` or `value if false` may be blank, in which case a blank string ("") is used for the value and both may also be an entirely new template string as seen in the above example. Using this format, template strings may be nested inside each other to form complex `if-then-else` statements. @@ -660,13 +660,13 @@ The configuration file is a plain text file in [TOML](https://toml.io/en/) forma #### Run commands on exported photos for post-processing -You can use the `--post-command` option to run one or more commands against exported files. The `--post-command` option takes two arguments: CATEGORY and COMMAND. CATEGORY is a string that describes which category of file to run the command against. The available categories are described in the help text available via: `osxphotos help export`. For example, the `exported` category includes all exported photos and the `skipped` category includes all photos that were skipped when running export with `--update`. COMMAND is an osxphotos template string which will be rendered then passed to the shell for execution. +You can use the `--post-command` option to run one or more commands against exported files. The `--post-command` option takes two arguments: CATEGORY and COMMAND. CATEGORY is a string that describes which category of file to run the command against. The available categories are described in the help text available via: `osxphotos help export`. For example, the `exported` category includes all exported photos and the `skipped` category includes all photos that were skipped when running export with `--update`. COMMAND is an osxphotos template string which will be rendered then passed to the shell for execution. For example, the following command generates a log of all exported files and their associated keywords: `osxphotos export /path/to/export --post-command exported "echo {shell_quote,{filepath}{comma}{,+keyword,}} >> {shell_quote,{export_dir}/exported.txt}"` -The special template field `{shell_quote}` ensures a string is properly quoted for execution in the shell. For example, it's possible that a file path or keyword in this example has a space in the value and if not properly quoted, this would cause an error in the execution of the command. When running commands, the template `{filepath}` is set to the full path of the exported file and `{export_dir}` is set to the full path of the base export directory. +The special template field `{shell_quote}` ensures a string is properly quoted for execution in the shell. For example, it's possible that a file path or keyword in this example has a space in the value and if not properly quoted, this would cause an error in the execution of the command. When running commands, the template `{filepath}` is set to the full path of the exported file and `{export_dir}` is set to the full path of the base export directory. Explanation of the template string: @@ -677,7 +677,7 @@ Explanation of the template string: │ │ │ │ └───> filepath of the exported file │ │ │ - └───> insert a comma + └───> insert a comma │ │ └───> join the list of keywords together with a "," │ @@ -1659,7 +1659,7 @@ Options: depending on system dark mode setting. -h, --help Show this message and exit. - Export + Export When exporting photos, osxphotos creates a database in the top-level export folder called '.osxphotos_export.db'. This database preserves state @@ -1691,7 +1691,7 @@ option to re-export the entire library thus rebuilding the '.osxphotos_export.db' database. - Extended Attributes + Extended Attributes Some options (currently '--finder-tag-template', '--finder-tag-keywords', '-xattr-template') write additional metadata accessible by Spotlight @@ -1759,300 +1759,300 @@ For additional information on extended attributes see: https://developer.apple _keys - Templating System + Templating System -The templating system converts one or template statements, written in -osxphotos metadata templating language, to one or more rendered values using -information from the photo being processed. +The templating system converts one or template statements, written in +osxphotos metadata templating language, to one or more rendered values using +information from the photo being processed. -In its simplest form, a template statement has the form: "{template_field}", -for example "{title}" which would resolve to the title of the photo. +In its simplest form, a template statement has the form: "{template_field}", +for example "{title}" which would resolve to the title of the photo. -Template statements may contain one or more modifiers. The full syntax is: +Template statements may contain one or more modifiers. The full syntax is: -"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] -conditional&combine_value?bool_value,default}posttext" +"pretext{delim+template_field:subfield(field_arg)|filter[find,replace] +conditional&combine_value?bool_value,default}posttext" -Template statements are white-space sensitive meaning that white space -(spaces, tabs) changes the meaning of the template statement. +Template statements are white-space sensitive meaning that white space +(spaces, tabs) changes the meaning of the template statement. -pretext and posttext are free form text. For example, if a photo has title -"My Photo Title" the template statement "The title of the photo is {title}", -resolves to "The title of the photo is My Photo Title". The pretext in this -example is "The title if the photo is " and the template_field is {title}. +pretext and posttext are free form text. For example, if a photo has title +"My Photo Title" the template statement "The title of the photo is {title}", +resolves to "The title of the photo is My Photo Title". The pretext in this +example is "The title if the photo is " and the template_field is {title}. -delim: optional delimiter string to use when expanding multi-valued template -values in-place +delim: optional delimiter string to use when expanding multi-valued template +values in-place -+: If present before template name, expands the template in place. If delim -not provided, values are joined with no delimiter. ++: If present before template name, expands the template in place. If delim +not provided, values are joined with no delimiter. -e.g. if Photo keywords are ["foo","bar"]: +e.g. if Photo keywords are ["foo","bar"]: - • "{keyword}" renders to "foo", "bar" - • "{,+keyword}" renders to: "foo,bar" - • "{; +keyword}" renders to: "foo; bar" - • "{+keyword}" renders to "foobar" + • "{keyword}" renders to "foo", "bar" + • "{,+keyword}" renders to: "foo,bar" + • "{; +keyword}" renders to: "foo; bar" + • "{+keyword}" renders to "foobar" template_field: The template field to resolve. See Template Substitutions for -full list of template fields. +full list of template fields. -:subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make}; -the template_field is exiftool and the sub-field is IPTC:Make. +:subfield: Some templates have sub-fields, For example, {exiftool:IPTC:Make}; +the template_field is exiftool and the sub-field is IPTC:Make. -(field_arg): optional arguments to pass to the field; for example, with -{folder_album} this is used to pass the path separator used for joining -folders and albums when rendering the field (default is "/" for -{folder_album}). +(field_arg): optional arguments to pass to the field; for example, with +{folder_album} this is used to pass the path separator used for joining +folders and albums when rendering the field (default is "/" for +{folder_album}). -|filter: You may optionally append one or more filter commands to the end of -the template field using the vertical pipe ('|') symbol. Filters may be -combined, separated by '|' as in: {keyword|capitalize|parens}. +|filter: You may optionally append one or more filter commands to the end of +the template field using the vertical pipe ('|') symbol. Filters may be +combined, separated by '|' as in: {keyword|capitalize|parens}. -Valid filters are: +Valid filters are: - • lower: Convert value to lower case, e.g. 'Value' => 'value'. - • upper: Convert value to upper case, e.g. 'Value' => 'VALUE'. - • strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => - 'Value'. - • titlecase: Convert value to title case, e.g. 'my value' => 'My Value'. + • lower: Convert value to lower case, e.g. 'Value' => 'value'. + • upper: Convert value to upper case, e.g. 'Value' => 'VALUE'. + • strip: Strip whitespace from beginning/end of value, e.g. ' Value ' => + 'Value'. + • titlecase: Convert value to title case, e.g. 'my value' => 'My Value'. • capitalize: Capitalize first word of value and convert other words to lower - case, e.g. 'MY VALUE' => 'My value'. - • braces: Enclose value in curly braces, e.g. 'value => '{value}'. - • parens: Enclose value in parentheses, e.g. 'value' => '(value') - • brackets: Enclose value in brackets, e.g. 'value' => '[value]' - • shell_quote: Quotes the value for safe usage in the shell, e.g. My - file.jpeg => 'My file.jpeg'; only adds quotes if needed. - • function: Run custom python function to filter value; use in format - 'function:/path/to/file.py::function_name'. See example at + case, e.g. 'MY VALUE' => 'My value'. + • braces: Enclose value in curly braces, e.g. 'value => '{value}'. + • parens: Enclose value in parentheses, e.g. 'value' => '(value') + • brackets: Enclose value in brackets, e.g. 'value' => '[value]' + • shell_quote: Quotes the value for safe usage in the shell, e.g. My + file.jpeg => 'My file.jpeg'; only adds quotes if needed. + • function: Run custom python function to filter value; use in format + 'function:/path/to/file.py::function_name'. See example at https://github.com/RhetTbull/osxphotos/blob/master/examples/template_filter - .py - • split(x): Split value into a list of values using x as delimiter, e.g. - 'value1;value2' => ['value1', 'value2'] if used with split(;). - • autosplit: Automatically split delimited string into separate values; will + .py + • split(x): Split value into a list of values using x as delimiter, e.g. + 'value1;value2' => ['value1', 'value2'] if used with split(;). + • autosplit: Automatically split delimited string into separate values; will split strings delimited by comma, semicolon, or space, e.g. 'value1,value2' - => ['value1', 'value2']. + => ['value1', 'value2']. • chop(x): Remove x characters off the end of value, e.g. chop(1): 'Value' => 'Valu'; when applied to a list, chops characters from each list value, e.g. - chop(1): ['travel', 'beach']=> ['trave', 'beac']. - • chomp(x): Remove x characters from the beginning of value, e.g. chomp(1): + chop(1): ['travel', 'beach']=> ['trave', 'beac']. + • chomp(x): Remove x characters from the beginning of value, e.g. chomp(1): ['Value'] => ['alue']; when applied to a list, removes characters from each - list value, e.g. chomp(1): ['travel', 'beach']=> ['ravel', 'each']. - • sort: Sort list of values, e.g. ['c', 'b', 'a'] => ['a', 'b', 'c']. - • rsort: Sort list of values in reverse order, e.g. ['a', 'b', 'c'] => ['c', - 'b', 'a']. - • reverse: Reverse order of values, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a']. + list value, e.g. chomp(1): ['travel', 'beach']=> ['ravel', 'each']. + • sort: Sort list of values, e.g. ['c', 'b', 'a'] => ['a', 'b', 'c']. + • rsort: Sort list of values in reverse order, e.g. ['a', 'b', 'c'] => ['c', + 'b', 'a']. + • reverse: Reverse order of values, e.g. ['a', 'b', 'c'] => ['c', 'b', 'a']. • uniq: Remove duplicate values, e.g. ['a', 'b', 'c', 'b', 'a'] => ['a', 'b', - 'c']. - • join(x): Join list of values with delimiter x, e.g. join(,): ['a', 'b', - 'c'] => 'a,b,c'; the DELIM option functions similar to join(x) but with - DELIM, the join happens before being passed to any filters.May optionally - be used without an argument, that is 'join()' which joins values together - with no delimiter. e.g. join(): ['a', 'b', 'c'] => 'abc'. - • append(x): Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => - ['a', 'b', 'c', 'd']. - • prepend(x): Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] - => ['d', 'a', 'b', 'c']. - • appends(x): Append s[tring] Append x to each value of list of values, e.g. - appends(d): ['a', 'b', 'c'] => ['ad', 'bd', 'cd']. - • prepends(x): Prepend s[tring] x to each value of list of values, e.g. - prepends(d): ['a', 'b', 'c'] => ['da', 'db', 'dc']. + 'c']. + • join(x): Join list of values with delimiter x, e.g. join(,): ['a', 'b', + 'c'] => 'a,b,c'; the DELIM option functions similar to join(x) but with + DELIM, the join happens before being passed to any filters.May optionally + be used without an argument, that is 'join()' which joins values together + with no delimiter. e.g. join(): ['a', 'b', 'c'] => 'abc'. + • append(x): Append x to list of values, e.g. append(d): ['a', 'b', 'c'] => + ['a', 'b', 'c', 'd']. + • prepend(x): Prepend x to list of values, e.g. prepend(d): ['a', 'b', 'c'] + => ['d', 'a', 'b', 'c']. + • appends(x): Append s[tring] Append x to each value of list of values, e.g. + appends(d): ['a', 'b', 'c'] => ['ad', 'bd', 'cd']. + • prepends(x): Prepend s[tring] x to each value of list of values, e.g. + prepends(d): ['a', 'b', 'c'] => ['da', 'db', 'dc']. • remove(x): Remove x from list of values, e.g. remove(b): ['a', 'b', 'c'] => - ['a', 'c']. - • slice(start:stop:step): Slice list using same semantics as Python's list + ['a', 'c']. + • slice(start:stop:step): Slice list using same semantics as Python's list slicing, e.g. slice(1:3): ['a', 'b', 'c', 'd'] => ['b', 'c']; slice(1:4:2): - ['a', 'b', 'c', 'd'] => ['b', 'd']; slice(1:): ['a', 'b', 'c', 'd'] => - ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; - slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also - sslice(). + ['a', 'b', 'c', 'd'] => ['b', 'd']; slice(1:): ['a', 'b', 'c', 'd'] => + ['b', 'c', 'd']; slice(:-1): ['a', 'b', 'c', 'd'] => ['a', 'b', 'c']; + slice(::-1): ['a', 'b', 'c', 'd'] => ['d', 'c', 'b', 'a']. See also + sslice(). • sslice(start:stop:step): [s(tring) slice] Slice values in a list using same - semantics as Python's string slicing, e.g. sslice(1:3):'abcd => 'bc'; - sslice(1:4:2): 'abcd' => 'bd', etc. See also slice(). - • filter(x): Filter list of values using predicate x; for example, - {folder_album|filter(contains Events)} returns only folders/albums - containing the word 'Events' in their path. - • int: Convert values in list to integer, e.g. 1.0 => 1. If value cannot be - converted to integer, remove value from list. ['1.1', 'x'] => ['1']. See - also float. - • float: Convert values in list to floating point number, e.g. 1 => 1.0. If - value cannot be converted to float, remove value from list. ['1', 'x'] => - ['1.0']. See also int. - -e.g. if Photo keywords are ["FOO","bar"]: - - • "{keyword|lower}" renders to "foo", "bar" - • "{keyword|upper}" renders to: "FOO", "BAR" - • "{keyword|capitalize}" renders to: "Foo", "Bar" - • "{keyword|lower|parens}" renders to: "(foo)", "(bar)" - -e.g. if Photo description is "my description": - - • "{descr|titlecase}" renders to: "My Description" - -e.g. If Photo is in Album1 in Folder1: - - • "{folder_album}" renders to ["Folder1/Album1"] - • "{folder_album(>)}" renders to ["Folder1>Album1"] - • "{folder_album()}" renders to ["Folder1Album1"] - -[find,replace]: optional text replacement to perform on rendered template -value. For example, to replace "/" in an album name, you could use the -template "{album[/,-]}". Multiple replacements can be made by appending "|" -and adding another find|replace pair. e.g. to replace both "/" and ":" in -album name: "{album[/,-|:,-]}". find/replace pairs are not limited to single -characters. The "|" character cannot be used in a find/replace pair. - -conditional: optional conditional expression that is evaluated as boolean -(True/False) for use with the ?bool_value modifier. Conditional expressions -take the form 'not operator value' where not is an optional modifier that -negates the operator. Note: the space before the conditional expression is + semantics as Python's string slicing, e.g. sslice(1:3):'abcd => 'bc'; + sslice(1:4:2): 'abcd' => 'bd', etc. See also slice(). + • filter(x): Filter list of values using predicate x; for example, + {folder_album|filter(contains Events)} returns only folders/albums + containing the word 'Events' in their path. + • int: Convert values in list to integer, e.g. 1.0 => 1. If value cannot be + converted to integer, remove value from list. ['1.1', 'x'] => ['1']. See + also float. + • float: Convert values in list to floating point number, e.g. 1 => 1.0. If + value cannot be converted to float, remove value from list. ['1', 'x'] => + ['1.0']. See also int. + +e.g. if Photo keywords are ["FOO","bar"]: + + • "{keyword|lower}" renders to "foo", "bar" + • "{keyword|upper}" renders to: "FOO", "BAR" + • "{keyword|capitalize}" renders to: "Foo", "Bar" + • "{keyword|lower|parens}" renders to: "(foo)", "(bar)" + +e.g. if Photo description is "my description": + + • "{descr|titlecase}" renders to: "My Description" + +e.g. If Photo is in Album1 in Folder1: + + • "{folder_album}" renders to ["Folder1/Album1"] + • "{folder_album(>)}" renders to ["Folder1>Album1"] + • "{folder_album()}" renders to ["Folder1Album1"] + +[find,replace]: optional text replacement to perform on rendered template +value. For example, to replace "/" in an album name, you could use the +template "{album[/,-]}". Multiple replacements can be made by appending "|" +and adding another find|replace pair. e.g. to replace both "/" and ":" in +album name: "{album[/,-|:,-]}". find/replace pairs are not limited to single +characters. The "|" character cannot be used in a find/replace pair. + +conditional: optional conditional expression that is evaluated as boolean +(True/False) for use with the ?bool_value modifier. Conditional expressions +take the form 'not operator value' where not is an optional modifier that +negates the operator. Note: the space before the conditional expression is required if you use a conditional expression. Valid comparison operators are: - • contains: template field contains value, similar to python's in - • matches: template field contains exactly value, unlike contains: does not - match partial matches - • startswith: template field starts with value - • endswith: template field ends with value - • <=: template field is less than or equal to value - • >=: template field is greater than or equal to value - • <: template field is less than value - • >: template field is greater than value - • ==: template field equals value - • !=: template field does not equal value - -The value part of the conditional expression is treated as a bare (unquoted) -word/phrase. Multiple values may be separated by '|' (the pipe symbol). -value is itself a template statement so you can use one or more template -fields in value which will be resolved before the comparison occurs. - -For example: - - • {keyword matches Beach} resolves to True if 'Beach' is a keyword. It would - not match keyword 'BeachDay'. - • {keyword contains Beach} resolves to True if any keyword contains the word - 'Beach' so it would match both 'Beach' and 'BeachDay'. - • {photo.score.overall > 0.7} resolves to True if the photo's overall - aesthetic score is greater than 0.7. - • {keyword|lower contains beach} uses the lower case filter to do - case-insensitive matching to match any keyword that contains the word - 'beach'. - • {keyword|lower not contains beach} uses the not modifier to negate the - comparison so this resolves to True if there is no keyword that matches - 'beach'. - -Examples: to export photos that contain certain keywords with the osxphotos -export command's --directory option: - ---directory "{keyword|lower matches -travel|vacation?Travel-Photos,Not-Travel-Photos}" - -This exports any photo that has keywords 'travel' or 'vacation' into a -directory 'Travel-Photos' and all other photos into directory -'Not-Travel-Photos'. - -This can be used to rename files as well, for example: --filename -"{favorite?Favorite-{original_name},{original_name}}" - -This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where -'ImageName.jpg' is the original name of the photo) and all other photos with -the unmodified original name. - -&combine_value: Template fields may be combined with another template -statement to return multiple values. The combine_value is another template -statement. For example, the template {created.year&{folder_album,}} would -resolve to ["1999", "Vacation"] if the photo was created in 1999 and was in -the album Vacation. Because the combine_value is a template statement, -multiple templates may be combined together by nesting the combine operator: -{template1&{template2&{template3,},},}. In this example, a null default value -is used to prevent the default value from being combined if any of the nested -templates does not resolve to a value - -?bool_value: Template fields may be evaluated as boolean (True/False) by -appending "?" after the field name (and following "(field_arg)" or + • contains: template field contains value, similar to python's in + • matches: template field contains exactly value, unlike contains: does not + match partial matches + • startswith: template field starts with value + • endswith: template field ends with value + • <=: template field is less than or equal to value + • >=: template field is greater than or equal to value + • <: template field is less than value + • >: template field is greater than value + • ==: template field equals value + • !=: template field does not equal value + +The value part of the conditional expression is treated as a bare (unquoted) +word/phrase. Multiple values may be separated by '|' (the pipe symbol). +value is itself a template statement so you can use one or more template +fields in value which will be resolved before the comparison occurs. + +For example: + + • {keyword matches Beach} resolves to True if 'Beach' is a keyword. It would + not match keyword 'BeachDay'. + • {keyword contains Beach} resolves to True if any keyword contains the word + 'Beach' so it would match both 'Beach' and 'BeachDay'. + • {photo.score.overall > 0.7} resolves to True if the photo's overall + aesthetic score is greater than 0.7. + • {keyword|lower contains beach} uses the lower case filter to do + case-insensitive matching to match any keyword that contains the word + 'beach'. + • {keyword|lower not contains beach} uses the not modifier to negate the + comparison so this resolves to True if there is no keyword that matches + 'beach'. + +Examples: to export photos that contain certain keywords with the osxphotos +export command's --directory option: + +--directory "{keyword|lower matches +travel|vacation?Travel-Photos,Not-Travel-Photos}" + +This exports any photo that has keywords 'travel' or 'vacation' into a +directory 'Travel-Photos' and all other photos into directory +'Not-Travel-Photos'. + +This can be used to rename files as well, for example: --filename +"{favorite?Favorite-{original_name},{original_name}}" + +This renames any photo that is a favorite as 'Favorite-ImageName.jpg' (where +'ImageName.jpg' is the original name of the photo) and all other photos with +the unmodified original name. + +&combine_value: Template fields may be combined with another template +statement to return multiple values. The combine_value is another template +statement. For example, the template {created.year&{folder_album,}} would +resolve to ["1999", "Vacation"] if the photo was created in 1999 and was in +the album Vacation. Because the combine_value is a template statement, +multiple templates may be combined together by nesting the combine operator: +{template1&{template2&{template3,},},}. In this example, a null default value +is used to prevent the default value from being combined if any of the nested +templates does not resolve to a value + +?bool_value: Template fields may be evaluated as boolean (True/False) by +appending "?" after the field name (and following "(field_arg)" or "[find/replace]". If a field is True (e.g. photo is HDR and field is "{hdr}") -or has any value, the value following the "?" will be used to render the -template instead of the actual field value. If the template field evaluates +or has any value, the value following the "?" will be used to render the +template instead of the actual field value. If the template field evaluates to False (e.g. in above example, photo is not HDR) or has no value (e.g. photo -has no title and field is "{title}") then the default value following a "," -will be used. +has no title and field is "{title}") then the default value following a "," +will be used. -e.g. if photo is an HDR image, +e.g. if photo is an HDR image, - • "{hdr?ISHDR,NOTHDR}" renders to "ISHDR" + • "{hdr?ISHDR,NOTHDR}" renders to "ISHDR" -and if it is not an HDR image, +and if it is not an HDR image, - • "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR" + • "{hdr?ISHDR,NOTHDR}" renders to "NOTHDR" -,default: optional default value to use if the template name has no value. +,default: optional default value to use if the template name has no value. This modifier is also used for the value if False for boolean-type fields (see -above) as well as to hold a sub-template for values like {created.strftime}. -If no default value provided, "_" is used. +above) as well as to hold a sub-template for values like {created.strftime}. +If no default value provided, "_" is used. -e.g., if photo has no title set, +e.g., if photo has no title set, - • "{title}" renders to "_" - • "{title,I have no title}" renders to "I have no title" + • "{title}" renders to "_" + • "{title,I have no title}" renders to "I have no title" -Template fields such as created.strftime use the default value to pass the -template to use for strftime. +Template fields such as created.strftime use the default value to pass the +template to use for strftime. -e.g., if photo date is 4 February 2020, 19:07:38, +e.g., if photo date is 4 February 2020, 19:07:38, - • "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738" + • "{created.strftime,%Y-%m-%d-%H%M%S}" renders to "2020-02-04-190738" -Some template fields such as "{media_type}" use the default value to allow -customization of the output. For example, "{media_type}" resolves to the -special media type of the photo such as panorama or selfie. You may use the -default value to override these in form: -"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if -photo was a time_lapse photo, media_type would resolve to vidéo_accélérée -instead of time_lapse. +Some template fields such as "{media_type}" use the default value to allow +customization of the output. For example, "{media_type}" resolves to the +special media type of the photo such as panorama or selfie. You may use the +default value to override these in form: +"{media_type,video=vidéo;time_lapse=vidéo_accélérée}". In this example, if +photo was a time_lapse photo, media_type would resolve to vidéo_accélérée +instead of time_lapse. -Either or both bool_value or default (False value) may be empty which would -result in empty string "" when rendered. +Either or both bool_value or default (False value) may be empty which would +result in empty string "" when rendered. -If you want to include "{" or "}" in the output, use "{openbrace}" or -"{closebrace}" template substitution. +If you want to include "{" or "}" in the output, use "{openbrace}" or +"{closebrace}" template substitution. -e.g. "{created.year}/{openbrace}{title}{closebrace}" would result in -"2020/{Photo Title}". +e.g. "{created.year}/{openbrace}{title}{closebrace}" would result in +"2020/{Photo Title}". -Variables +Variables You can define variables for later use in the template string using the format -{var:NAME,VALUE} where VALUE is a template statement. Variables may then be -referenced using the format %NAME. For example: {var:foo,bar} defines the -variable %foo to have value bar. This can be useful if you want to re-use a -complex template value in multiple places within your template string or for -allowing the use of characters that would otherwise be prohibited in a -template string. For example, the "pipe" (|) character is not allowed in a -find/replace pair but you can get around this limitation like so: -{var:pipe,{pipe}}{title[-,%pipe]} which replaces the - character with | (the -value of %pipe). - -Another use case for variables is filtering combined template values. For -example, using the &combine_value mechanism to combine two template values -that might result in duplicate values, you could do the following: +{var:NAME,VALUE} where VALUE is a template statement. Variables may then be +referenced using the format %NAME. For example: {var:foo,bar} defines the +variable %foo to have value bar. This can be useful if you want to re-use a +complex template value in multiple places within your template string or for +allowing the use of characters that would otherwise be prohibited in a +template string. For example, the "pipe" (|) character is not allowed in a +find/replace pair but you can get around this limitation like so: +{var:pipe,{pipe}}{title[-,%pipe]} which replaces the - character with | (the +value of %pipe). + +Another use case for variables is filtering combined template values. For +example, using the &combine_value mechanism to combine two template values +that might result in duplicate values, you could do the following: {var:myvar,{template1&{template2,},}}{%myvar|uniq} which allows the use of the -uniq filter against the combined template values. +uniq filter against the combined template values. -Variables can also be referenced as fields in the template string, for -example: {var:year,{created.year}}{original_name}-{%year}. In some cases, use -of variables can make your template string more readable. Variables can be -used as template fields, as values for filters, as values for conditional +Variables can also be referenced as fields in the template string, for +example: {var:year,{created.year}}{original_name}-{%year}. In some cases, use +of variables can make your template string more readable. Variables can be +used as template fields, as values for filters, as values for conditional operations, or as default values. When used as a conditional value or default value, variables should be treated like any other field and enclosed in braces -as conditional and default values are evaluated as template strings. For -example: {var:name,Katie}{person contains {%name}?{%name},Not-{%name}}. +as conditional and default values are evaluated as template strings. For +example: {var:name,Katie}{person contains {%name}?{%name},Not-{%name}}. -If you need to use a % (percent sign character), you can escape the percent -sign by using %%. You can also use the {percent} template field where a -template field is required. For example: +If you need to use a % (percent sign character), you can escape the percent +sign by using %%. You can also use the {percent} template field where a +template field is required. For example: -{title[:,%%]} replaces the : with % and {title contains -Foo?{title}{percent},{title}} adds % to the title if it contains Foo. +{title[:,%%]} replaces the : with % and {title contains +Foo?{title}{percent},{title}} adds % to the title if it contains Foo. With the --directory and --filename options you may specify a template for the export directory or filename, respectively. The directory will be appended to @@ -2074,7 +2074,7 @@ corresponding value from the table below. Invalid substitutions will result in a an error and the script will abort. - Template Substitutions + Template Substitutions Substitution Description {name} Current filename of the photo @@ -2504,7 +2504,7 @@ Substitution Description {filepath} The full path to the exported file - Post Command + Post Command You can run commands on the exported photos for post-processing using the '-- post-command' option. '--post-command' is passed a CATEGORY and a COMMAND. @@ -2569,7 +2569,7 @@ first to ensure your commands are as expected. This will not actually run the commands but will print out the exact command string which would be executed. - Post Function + Post Function You can run your own python functions on the exported photos for post- processing using the '--post-function' option. '--post-function' is passed the diff --git a/dev_requirements.txt b/dev_requirements.txt index 7656bc1b2..80c0c08d2 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -6,11 +6,11 @@ furo isort m2r2==0.3.3.post2 pdbpp -pyinstaller==5.13.2 +pyinstaller>=6.11.1 pytest-cov==4.0.0 pytest-mock -pytest==7.4.0 -ruff==0.7.2 +pytest>=7.4.0,<8.0.0 +ruff==0.8.1 Sphinx sphinx_click sphinx_rtd_theme diff --git a/docs/.buildinfo b/docs/.buildinfo index fa00444ca..6d382f48a 100644 --- a/docs/.buildinfo +++ b/docs/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 -# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: a02785b8a9b27357a3304c5646993811 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 453689a36d886f59523b5b58791e91e0 tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/API_README.html b/docs/API_README.html index eaa8a8146..aec215206 100644 --- a/docs/API_README.html +++ b/docs/API_README.html @@ -5,12 +5,12 @@ - + OSXPhotos Python API - osxphotos 0.68.6 documentation - + - + @@ -69,7 +69,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -84,22 +84,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -113,6 +154,8 @@
Hide table of contents sidebar
+Skip to content +
@@ -130,7 +173,8 @@
@@ -182,11 +226,17 @@ Back to top
- +
@@ -196,7 +246,7 @@
-
+

OSXPhotos Python API

In addition to a command line interface, OSXPhotos provides a access to a Python API that allows you to easily access a Photos library, often with just a few lines of code.

@@ -1114,15 +1164,33 @@

original_filena

date

-

Returns the create date of the photo as a datetime.datetime object

+

Returns the create date of the photo as a timezone aware datetime.datetime object

+
+
+

tzoffset

+

Returns the timezone offset from UTC in seconds for the Photo creation date

+
+
+

tzname

+

Returns the timezone name for the Photos creation date; on Photos version < 5, returns None

+
+
+

date_original

+

Returns the original creation date of the photo as a timezone aware datetime.datetime object. +If user changed the photo’s date in Photos, this will return the original date Photos assigned +as the creation date at the time of import. The original date is stored by Photos at import time +from the date in the photo’s EXIF data. If this is not set (photo had no EXIF date or photo was +imported on an older version of macOS that did not store original date) then date_original +returns the same value as date.

+

Photos 5+ only; on Photos < 5.0, this will return the same value as date.

date_added

-

Returns the date the photo was added to the Photos library as a timezone aware datetime.datetime object, or None if the data added cannot be determined

+

Returns the date the photo was added to the Photos library as a timezone aware datetime.datetime object in the local timezone, or None if the data added cannot be determined

date_modified

-

Returns the modification date of the photo as a datetime.datetime object or None if photo has no modification date

+

Returns the modification date of the photo as a timezone aware atetime.datetime object in the local timezone or None if photo has no modification date

description

@@ -1276,7 +1344,7 @@

intrash<

date_trashed

-

Returns the date the photo was placed in the trash as a datetime.datetime object or None if photo is not in the trash

+

Returns the date the photo was placed in the trash as a timezone aware datetime.datetime object in the local timezone or None if photo is not in the trash

location

@@ -1665,6 +1733,9 @@

ExifInfocamera_model: str codec: str lens_model: str +date: datetime.datetime | None +tzoffset: int | None +tzname: str | None

For example:

@@ -3409,6 +3480,9 @@

Additional Examplesfilename
  • original_filename
  • date
  • +
  • tzoffset
  • +
  • tzname
  • +
  • date_original
  • date_added
  • date_modified
  • description
  • @@ -3680,9 +3754,9 @@

    Additional Examples - + - + diff --git a/docs/_modules/index.html b/docs/_modules/index.html index 39723b923..d506dec64 100644 --- a/docs/_modules/index.html +++ b/docs/_modules/index.html @@ -4,12 +4,12 @@ - + Overview: module code - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +
    Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,7 +239,7 @@
    -
    - + - + diff --git a/docs/_modules/osxphotos/_constants.html b/docs/_modules/osxphotos/_constants.html index 7a33d8ebc..99f478e55 100644 --- a/docs/_modules/osxphotos/_constants.html +++ b/docs/_modules/osxphotos/_constants.html @@ -4,12 +4,12 @@ - + osxphotos._constants - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,11 +239,9 @@
    -
    +

    Source code for osxphotos._constants

    -""" Constants used by osxphotos """
    -
    -from __future__ import annotations
    +from __future__ import annotations
     
     import logging
     import os.path
    @@ -212,10 +255,6 @@ 

    Source code for osxphotos._constants

     
     OSXPHOTOS_URL = "https://github.com/RhetTbull/osxphotos"
     
    -# Time delta: add this to Photos times to get unix time
    -# Apple Epoch is Jan 1, 2001
    -TIME_DELTA = (datetime(2001, 1, 1, 0, 0) - datetime(1970, 1, 1, 0, 0)).total_seconds()
    -
     # which Photos library database versions have been tested
     # Photos 2.0 (10.12.6) == 2622
     # Photos 3.0 (10.13.6) == 3301
    @@ -826,9 +865,9 @@ 

    Source code for osxphotos._constants

         
       
    - + - + diff --git a/docs/_modules/osxphotos/albuminfo.html b/docs/_modules/osxphotos/albuminfo.html index 052741e2f..de1352074 100644 --- a/docs/_modules/osxphotos/albuminfo.html +++ b/docs/_modules/osxphotos/albuminfo.html @@ -4,12 +4,12 @@ - + osxphotos.albuminfo - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,7 +239,7 @@
    -
    +

    Source code for osxphotos.albuminfo

     """
     AlbumInfo and FolderInfo classes for dealing with albums and folders
    @@ -217,10 +262,9 @@ 

    Source code for osxphotos.albuminfo

         _PHOTOS_5_ALBUM_KIND,
         _PHOTOS_5_FOLDER_KIND,
         _PHOTOS_5_VERSION,
    -    TIME_DELTA,
         AlbumSortOrder,
     )
    -from .datetime_utils import get_local_tz
    +from .photos_datetime import photos_datetime_local
     
     __all__ = [
         "sort_list_by_keys",
    @@ -265,9 +309,6 @@ 

    Source code for osxphotos.albuminfo

             self._creation_date_timestamp = self._db._dbalbum_details[uuid]["creation_date"]
             self._start_date_timestamp = self._db._dbalbum_details[uuid]["start_date"]
             self._end_date_timestamp = self._db._dbalbum_details[uuid]["end_date"]
    -        self._local_tz = get_local_tz(
    -            datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
    -        )
     
         @property
         def uuid(self):
    @@ -280,20 +321,9 @@ 

    Source code for osxphotos.albuminfo

             try:
                 return self._creation_date
             except AttributeError:
    -            try:
    -                self._creation_date = (
    -                    datetime.fromtimestamp(
    -                        self._creation_date_timestamp + TIME_DELTA
    -                    ).astimezone(tz=self._local_tz)
    -                    if self._creation_date_timestamp
    -                    else datetime(1970, 1, 1, 0, 0, 0).astimezone(
    -                        tz=timezone(timedelta(0))
    -                    )
    -                )
    -            except ValueError:
    -                self._creation_date = datetime(1970, 1, 1, 0, 0, 0).astimezone(
    -                    tz=timezone(timedelta(0))
    -                )
    +            self._creation_date = photos_datetime_local(
    +                self._creation_date_timestamp, True
    +            )
                 return self._creation_date
     
         @property
    @@ -303,16 +333,7 @@ 

    Source code for osxphotos.albuminfo

             try:
                 return self._start_date
             except AttributeError:
    -            try:
    -                self._start_date = (
    -                    datetime.fromtimestamp(
    -                        self._start_date_timestamp + TIME_DELTA
    -                    ).astimezone(tz=self._local_tz)
    -                    if self._start_date_timestamp
    -                    else None
    -                )
    -            except ValueError:
    -                self._start_date = None
    +            self._start_date = photos_datetime_local(self._start_date_timestamp, False)
                 return self._start_date
     
         @property
    @@ -323,16 +344,7 @@ 

    Source code for osxphotos.albuminfo

             try:
                 return self._end_date
             except AttributeError:
    -            try:
    -                self._end_date = (
    -                    datetime.fromtimestamp(
    -                        self._end_date_timestamp + TIME_DELTA
    -                    ).astimezone(tz=self._local_tz)
    -                    if self._end_date_timestamp
    -                    else None
    -                )
    -            except ValueError:
    -                self._end_date = None
    +            self._end_date = photos_datetime_local(self._end_date_timestamp, False)
                 return self._end_date
     
         @property
    @@ -533,9 +545,6 @@ 

    Source code for osxphotos.albuminfo

             self._start_date_timestamp = self._creation_date_timestamp
             self._end_date_timestamp = self._creation_date_timestamp
             self._title = import_session[2]
    -        self._local_tz = get_local_tz(
    -            datetime.fromtimestamp(self._creation_date_timestamp + TIME_DELTA)
    -        )
     
         @property
         def title(self):
    @@ -769,9 +778,9 @@ 

    Source code for osxphotos.albuminfo

         
       
    - + - + diff --git a/docs/_modules/osxphotos/exifinfo.html b/docs/_modules/osxphotos/exifinfo.html index ca1bee122..08d25a99c 100644 --- a/docs/_modules/osxphotos/exifinfo.html +++ b/docs/_modules/osxphotos/exifinfo.html @@ -4,12 +4,12 @@ - + osxphotos.exifinfo - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,41 +239,88 @@
    -
    +

    Source code for osxphotos.exifinfo

     """ ExifInfo class to expose EXIF info from the library """
     
    +from __future__ import annotations
    +
    +import datetime
     from dataclasses import dataclass
    +from typing import Any
     
    -__all__ = ["ExifInfo"]
    +from osxphotos.photos_datetime import photos_datetime
    +
    +__all__ = ["ExifInfo", "exifinfo_factory"]
     
     
     
    [docs] @dataclass(frozen=True) class ExifInfo: - """EXIF info associated with a photo from the Photos library""" + """Original EXIF info associated with a photo from the Photos library""" + + flash_fired: bool | None = None + iso: int | None = None + metering_mode: int | None = None + sample_rate: int | None = None + track_format: int | None = None + white_balance: int | None = None + aperture: float | None = None + bit_rate: float | None = None + duration: float | None = None + exposure_bias: float | None = None + focal_length: float | None = None + fps: float | None = None + latitude: float | None = None + longitude: float | None = None + shutter_speed: float | None = None + camera_make: str | None = None + camera_model: str | None = None + codec: str | None = None + lens_model: str | None = None + date: datetime.datetime | None = None + tzoffset: int | None = None + tzname: str | None = None
    + + - flash_fired: bool - iso: int - metering_mode: int - sample_rate: int - track_format: int - white_balance: int - aperture: float - bit_rate: float - duration: float - exposure_bias: float - focal_length: float - fps: float - latitude: float - longitude: float - shutter_speed: float - camera_make: str - camera_model: str - codec: str - lens_model: str
    +def exifinfo_factory(data: dict[str, Any] | None) -> ExifInfo: + """Create an ExifInfo object from a dictionary of EXIF data""" + if data is None: + return ExifInfo() + exif_info = ExifInfo( + iso=data["ZISO"], + flash_fired=True if data["ZFLASHFIRED"] == 1 else False, + metering_mode=data["ZMETERINGMODE"], + sample_rate=data["ZSAMPLERATE"], + track_format=data["ZTRACKFORMAT"], + white_balance=data["ZWHITEBALANCE"], + aperture=data["ZAPERTURE"], + bit_rate=data["ZBITRATE"], + duration=data["ZDURATION"], + exposure_bias=data["ZEXPOSUREBIAS"], + focal_length=data["ZFOCALLENGTH"], + fps=data["ZFPS"], + latitude=data["ZLATITUDE"], + longitude=data["ZLONGITUDE"], + shutter_speed=data["ZSHUTTERSPEED"], + camera_make=data["ZCAMERAMAKE"], + camera_model=data["ZCAMERAMODEL"], + codec=data["ZCODEC"], + lens_model=data["ZLENSMODEL"], + # ZDATECREATED, ZTIMEZONEOFFSET, ZTIMEZONENAME added in Ventura / Photos 8 so may not be present + tzoffset=data.get("ZTIMEZONEOFFSET"), + tzname=data.get("ZTIMEZONENAME"), + date=photos_datetime( + data.get("ZDATECREATED"), + data.get("ZTIMEZONEOFFSET"), + data.get("ZTIMEZONENAME"), + default=False, + ), + ) + return exif_info
    @@ -262,9 +354,9 @@

    Source code for osxphotos.exifinfo

         
       
    - + - + diff --git a/docs/_modules/osxphotos/exiftool.html b/docs/_modules/osxphotos/exiftool.html index e4517d144..883574ce5 100644 --- a/docs/_modules/osxphotos/exiftool.html +++ b/docs/_modules/osxphotos/exiftool.html @@ -4,12 +4,12 @@ - - osxphotos.exiftool - osxphotos 0.68.3 documentation + + osxphotos.exiftool - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -123,13 +166,14 @@
    @@ -146,7 +190,7 @@ + - + diff --git a/docs/_modules/osxphotos/exifwriter.html b/docs/_modules/osxphotos/exifwriter.html index 666bb2148..98ce1453d 100644 --- a/docs/_modules/osxphotos/exifwriter.html +++ b/docs/_modules/osxphotos/exifwriter.html @@ -4,12 +4,12 @@ - + osxphotos.exifwriter - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,7 +239,7 @@
    -
    +

    Source code for osxphotos.exifwriter

     """Write metadata to files using exiftool"""
     
    @@ -202,6 +247,7 @@ 

    Source code for osxphotos.exifwriter

     
     import contextlib
     import dataclasses
    +import datetime
     import json
     import logging
     import os
    @@ -211,7 +257,7 @@ 

    Source code for osxphotos.exifwriter

     from typing import TYPE_CHECKING, Any
     
     from ._constants import _MAX_IPTC_KEYWORD_LEN, _OSXPHOTOS_NONE_SENTINEL, _UNKNOWN_PERSON
    -from .datetime_utils import datetime_tz_to_utc
    +from .datetime_utils import datetime_has_tz, datetime_tz_to_utc
     from .exiftool import ExifTool, ExifToolCaching
     from .exportoptions import ExportOptions
     from .phototemplate import RenderOptions
    @@ -431,8 +477,11 @@ 

    Source code for osxphotos.exifwriter

                 EXIF:GPSLatitude, EXIF:GPSLongitude
                 EXIF:GPSPosition
                 EXIF:DateTimeOriginal
    -            EXIF:OffsetTimeOriginal
    +            EXIF:SubSecTimeOriginal
    +            EXIF:OffsetTimeOriginal (UTC offset for DateTimeOriginal)
                 EXIF:ModifyDate
    +            EXIF:SubSecTime
    +            EXIF:OffsetTime (UTC offset for ModifyDate)
                 IPTC:DateCreated
                 IPTC:TimeCreated
                 QuickTime:CreationDate
    @@ -622,18 +671,16 @@ 

    Source code for osxphotos.exifwriter

     
             if options.datetime:
                 date = self.photo.date
    -            offsettime = date.strftime("%z")
    -            # find timezone offset in format "-04:00"
    -            offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime)
    -            offset = offset[0]  # findall returns list of tuples
    -            offsettime = f"{offset[0]}{offset[1]}:{offset[2]}"
    +            offsettime = utc_offset_time(date)
    +            subsec = subsec_time(date)
     
                 # exiftool expects format to "2015:01:18 12:00:00"
    -            datetimeoriginal = date.strftime("%Y:%m:%d %H:%M:%S")
    +            datetimeoriginal = exiftool_datetime(date)
     
                 if self.photo.isphoto:
                     exif["EXIF:DateTimeOriginal"] = datetimeoriginal
                     exif["EXIF:CreateDate"] = datetimeoriginal
    +                exif["EXIF:SubSecTimeOriginal"] = subsec
                     exif["EXIF:OffsetTimeOriginal"] = offsettime
     
                     dateoriginal = date.strftime("%Y:%m:%d")
    @@ -646,13 +693,15 @@ 

    Source code for osxphotos.exifwriter

                         self.photo.date_modified is not None
                         and not options.ignore_date_modified
                     ):
    -                    exif["EXIF:ModifyDate"] = self.photo.date_modified.strftime(
    -                        "%Y:%m:%d %H:%M:%S"
    +                    exif["EXIF:ModifyDate"] = exiftool_datetime(
    +                        self.photo.date_modified
                         )
    +                    exif["EXIF:SubSecTime"] = subsec_time(self.photo.date_modified)
    +                    exif["EXIF:OffsetTime"] = utc_offset_time(self.photo.date_modified)
                     else:
    -                    exif["EXIF:ModifyDate"] = self.photo.date.strftime(
    -                        "%Y:%m:%d %H:%M:%S"
    -                    )
    +                    exif["EXIF:ModifyDate"] = exiftool_datetime(self.photo.date)
    +                    exif["EXIF:SubSectime"] = subsec
    +                    exif["EXIF:OffsetTime"] = offsettime
                 elif self.photo.ismovie:
                     # QuickTime spec specifies times in UTC
                     # QuickTime:CreateDate and ModifyDate are in UTC w/ no timezone
    @@ -667,14 +716,14 @@ 

    Source code for osxphotos.exifwriter

                     exif["QuickTime:ContentCreateDate"] = f"{datetimeoriginal}{offsettime}"
     
                     date_utc = datetime_tz_to_utc(date)
    -                creationdate = date_utc.strftime("%Y:%m:%d %H:%M:%S")
    +                creationdate = exiftool_datetime(date_utc)
                     exif["QuickTime:CreateDate"] = creationdate
                     if self.photo.date_modified is None or options.ignore_date_modified:
                         exif["QuickTime:ModifyDate"] = creationdate
                     else:
    -                    exif["QuickTime:ModifyDate"] = datetime_tz_to_utc(
    -                        self.photo.date_modified
    -                    ).strftime("%Y:%m:%d %H:%M:%S")
    +                    exif["QuickTime:ModifyDate"] = exiftool_datetime(
    +                        datetime_tz_to_utc(self.photo.date_modified)
    +                    )
     
             # if photo in PNG remove any IPTC tags (#1031)
             if self.photo.isphoto and self.photo.uti == "public.png":
    @@ -736,28 +785,44 @@ 

    Source code for osxphotos.exifwriter

             Returns: JSON string for dict with exiftool tags / values
     
             Exports the following:
    -            EXIF:ImageDescription
    +            EXIF:ImageDescription (may include template)
                 XMP:Description (may include template)
    -            IPTC:CaptionAbstract
                 XMP:Title
                 IPTC:ObjectName
    -            XMP:TagsList
    +            XMP:TagsList (may include album name, person name, or template)
                 IPTC:Keywords (may include album name, person name, or template)
    -            XMP:Subject (set to keywords + person)
    +            IPTC:Caption-Abstract
    +            XMP:Subject (set to keywords + persons)
                 XMP:PersonInImage
                 EXIF:GPSLatitudeRef, EXIF:GPSLongitudeRef
                 EXIF:GPSLatitude, EXIF:GPSLongitude
                 EXIF:GPSPosition
                 EXIF:DateTimeOriginal
    -            EXIF:OffsetTimeOriginal
    +            EXIF:SubSecTimeOriginal
    +            EXIF:OffsetTimeOriginal (UTC offset for DateTimeOriginal)
                 EXIF:ModifyDate
    -            IPTC:DigitalCreationDate
    +            EXIF:SubSecTime
    +            EXIF:OffsetTime (UTC offset for ModifyDate)
                 IPTC:DateCreated
    +            IPTC:TimeCreated
                 QuickTime:CreationDate
    +            QuickTime:ContentCreateDate
                 QuickTime:CreateDate (UTC)
                 QuickTime:ModifyDate (UTC)
                 QuickTime:GPSCoordinates
                 UserData:GPSCoordinates
    +            XMP:Rating
    +            XMP:RegionAppliedToDimensionsW
    +            XMP:RegionAppliedToDimensionsH
    +            XMP:RegionAppliedToDimensionsUnit
    +            XMP:RegionName
    +            XMP:RegionType
    +            XMP:RegionAreaX
    +            XMP:RegionAreaY
    +            XMP:RegionAreaW
    +            XMP:RegionAreaH
    +            XMP:RegionAreaUnit
    +            XMP:RegionPersonDisplayName
             """
     
             options = options or ExifOptions()
    @@ -774,6 +839,49 @@ 

    Source code for osxphotos.exifwriter

             return json.dumps([exif])
    + + +def utc_offset_time(dt: datetime.datetime) -> str: + """Find the UTC offset for a datetime in format expected by exiftool (+/-HH:MM) + + Args: + dt: datetime object to find offset for + + Returns: string with offset in format "+/-HH:MM + + Raises: + ValueError if datetime is not timezone aware or if timezone cannot be determined + """ + if not datetime_has_tz(dt): + raise ValueError("datetime must be timezone aware") + offsettime = dt.strftime("%z") + offset = re.findall(r"([+-]?)([\d]{2})([\d]{2})", offsettime) + if not offset: + raise ValueError(f"could not parse timezone from datetime {dt}") + offset = offset[0] # findall returns list of tuples + if len(offset) != 3: + raise ValueError(f"could not parse timezone from datetime {dt}") + offsettime = f"{offset[0]}{offset[1]}:{offset[2]}" + return offsettime + + +def exiftool_datetime(dt: datetime.datetime) -> str: + """Format a datetime to the format expected by exiftool (YYYY:MM:DD HH:MM:SS) + + Args: + dt: datetime to format + + Returns: string formatted as date/time value expected by exiftool in YYYY:MM:DD HH:MM:SS format + """ + return dt.strftime("%Y:%m:%d %H:%M:%S") + + +def subsec_time(dt: datetime.datetime) -> str: + """Return sub-second time as a string as expected by exiftool for EXIF:SubSecTime""" + # strftime("%f") returns microseconds but only want milliseconds + # if sub-second time is 0, it will return all zeros + # strip off trailing zeroes + return dt.strftime("%f")[:-3].rstrip("0")
    @@ -807,9 +915,9 @@

    Source code for osxphotos.exifwriter

         
       
    - + - + diff --git a/docs/_modules/osxphotos/iphoto.html b/docs/_modules/osxphotos/iphoto.html index 720b83432..79babeae3 100644 --- a/docs/_modules/osxphotos/iphoto.html +++ b/docs/_modules/osxphotos/iphoto.html @@ -4,12 +4,12 @@ - + osxphotos.iphoto - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -129,7 +172,8 @@
    @@ -184,7 +228,8 @@
    @@ -194,7 +239,7 @@
    -
    +

    Source code for osxphotos.iphoto

     """Support for iPhoto libraries
     
    @@ -251,7 +296,6 @@ 

    Source code for osxphotos.iphoto

         SIDECAR_EXIFTOOL,
         SIDECAR_JSON,
         SIDECAR_XMP,
    -    TIME_DELTA,
     )
     from .datetime_utils import datetime_has_tz, datetime_naive_to_local
     from .exiftool import ExifToolCaching, get_exiftool_path
    @@ -261,6 +305,7 @@ 

    Source code for osxphotos.iphoto

     from .photoexporter import PhotoExporter
     from .photoinfo import PhotoInfo
     from .photoquery import QueryOptions, photo_query
    +from .photos_datetime import photos_datetime, photos_datetime_local
     from .phototemplate import PhotoTemplate, RenderOptions
     from .platform import is_macos
     from .scoreinfo import ScoreInfo
    @@ -979,6 +1024,8 @@ 

    Source code for osxphotos.iphoto

                 else:
                     photo["path_edited"] = ""
     
    +
    +[docs] @cached_property def db_version(self) -> str: """Return the database version as stored in Library.apdb RKAdminData table""" @@ -994,8 +1041,11 @@

    Source code for osxphotos.iphoto

             conn = sqlite3.connect(library_db)
             cursor = conn.cursor()
             results = cursor.execute(query).fetchall()
    -        return ".".join(row[0] for row in results)
    +        return ".".join(row[0] for row in results)
    + +
    +[docs] @cached_property def photos_version(self) -> str: """Returns version of the library as a string""" @@ -1011,7 +1061,8 @@

    Source code for osxphotos.iphoto

             conn = sqlite3.connect(library_db)
             cursor = conn.cursor()
             results = cursor.execute(query).fetchall()
    -        return f"{results[0][0]} - {self.db_version}"
    +        return f"{results[0][0]} - {self.db_version}"
    +
    [docs] @@ -1279,25 +1330,28 @@

    Source code for osxphotos.iphoto

         @property
         def date(self) -> datetime.datetime:
             """Date photo was taken"""
    -        return iphoto_date_to_datetime(
    -            self._db._db_photos[self._uuid]["date_taken"],
    -            self._db._db_photos[self._uuid]["timezone"],
    +        return photos_datetime(
    +            timestamp=self._db._db_photos[self._uuid]["date_taken"],
    +            tzname=self._db._db_photos[self._uuid]["timezone"],
    +            default=True,
             )
     
         @property
         def date_modified(self) -> datetime.datetime:
             """Date modified in library"""
    -        return iphoto_date_to_datetime(
    -            self._db._db_photos[self._uuid]["date_modified"],
    -            self._db._db_photos[self._uuid]["timezone"],
    +        return photos_datetime(
    +            timestamp=self._db._db_photos[self._uuid]["date_modified"],
    +            tzname=self._db._db_photos[self._uuid]["timezone"],
    +            default=False,
             )
     
         @property
         def date_added(self) -> datetime.datetime:
             """Date added to library"""
    -        return iphoto_date_to_datetime(
    -            self._db._db_photos[self._uuid]["date_imported"],
    -            self._db._db_photos[self._uuid]["timezone"],
    +        return photos_datetime(
    +            timestamp=self._db._db_photos[self._uuid]["date_imported"],
    +            tzname=self._db._db_photos[self._uuid]["timezone"],
    +            default=True,
             )
     
         @property
    @@ -1307,7 +1361,7 @@ 

    Source code for osxphotos.iphoto

             if not tzname:
                 return 0
             tz = ZoneInfo(tzname)
    -        return int(tz.utcoffset(datetime.datetime.now()).total_seconds())
    +        return int(tz.utcoffset(self.date).total_seconds())
     
         @property
         def path(self) -> str | None:
    @@ -1493,6 +1547,8 @@ 

    Source code for osxphotos.iphoto

                     return iPhotoMomentInfo(event_data, self._db)
             return None
     
    +
    +[docs] @cached_property def fingerprint(self) -> str | None: """Returns fingerprint of original photo as a string; returns None if not available. On linux, returns None.""" @@ -1504,8 +1560,11 @@

    Source code for osxphotos.iphoto

                 logger.debug(f"Missing path, cannot compute fingerprint for {self.uuid}")
                 return None
     
    -        return fingerprint(self.path)
    +        return fingerprint(self.path)
    + +
    +[docs] @cached_property def exif_info(self) -> iPhotoExifInfo: """Return iPhotoExifInfo object for photo""" @@ -1533,7 +1592,8 @@

    Source code for osxphotos.iphoto

                 lens_model=exif_info.get("LensModel", ""),
                 software=exif_info.get("Software", ""),
                 dict=exif_info,
    -        )
    +        )
    + @property def burst_albums(self) -> list[str]: @@ -1566,11 +1626,14 @@

    Source code for osxphotos.iphoto

                 and self._db._db_photos[uuid]["burst_uuid"] == burst_uuid
             ]
     
    +
    +[docs] @cached_property def hexdigest(self) -> str: """Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo""" - return hexdigest(self._json_hexdigest()) + return hexdigest(self._json_hexdigest())
    + @property def score(self) -> ScoreInfo: @@ -1740,6 +1803,8 @@

    Source code for osxphotos.iphoto

             return template.render(template_str, options)
    +
    +[docs] @cached_property def exiftool(self) -> ExifToolCaching | None: """Returns a ExifToolCaching (read-only instance of ExifTool) object for the photo. @@ -1759,7 +1824,8 @@

    Source code for osxphotos.iphoto

                 logging.warning(
                     "exiftool not in path; download and install from https://exiftool.org/"
                 )
    -        return exiftool
    +        return exiftool
    +
    [docs] @@ -2655,7 +2721,7 @@

    Source code for osxphotos.iphoto

         def _date_created(self) -> datetime.datetime | None:
             """Date the event created in iPhoto."""
             # not common with Photos MomentInfo so leave private
    -        return naive_iphoto_date_to_datetime(self._event["date"])
    +        return photos_datetime_local(self._event["date"])
     
         @property
         def modification_date(self) -> datetime.datetime | None:
    @@ -2732,49 +2798,6 @@ 

    Source code for osxphotos.iphoto

     ### Utility functions ###
     
     
    -def iphoto_date_to_datetime(
    -    date: int | None, tz: str | None = None
    -) -> datetime.datetime:
    -    """ "Convert iPhoto date to datetime; if tz provided, will be timezone aware
    -
    -    Args:
    -        date: iPhoto date
    -        tz: timezone name
    -
    -    Returns:
    -        datetime.datetime
    -
    -    Note:
    -        If date is None or invalid, will return 1970-01-01 00:00:00
    -    """
    -    try:
    -        dt = datetime.datetime.fromtimestamp(date + TIME_DELTA)
    -    except (ValueError, TypeError):
    -        dt = datetime.datetime(1970, 1, 1)
    -    if tz:
    -        dt = dt.replace(tzinfo=ZoneInfo(tz))
    -    return dt
    -
    -
    -def naive_iphoto_date_to_datetime(date: int) -> datetime.datetime:
    -    """ "Convert iPhoto date to datetime with local timezone
    -
    -    Args:
    -        date: iPhoto date
    -
    -    Returns:
    -        timezone aware datetime.datetime in local timezone
    -
    -    Note:
    -        If date is invalid, will return 1970-01-01 00:00:00
    -    """
    -    try:
    -        dt = datetime.datetime.fromtimestamp(date + TIME_DELTA)
    -    except ValueError:
    -        dt = datetime.datetime(1970, 1, 1)
    -    return datetime_naive_to_local(dt)
    -
    -
     def default_return_value(name: str) -> Any:
         """Inspect name and return default value if there is one otherwise None
         optimized for PhotoInfo may not work for other classes.
    @@ -2882,9 +2905,9 @@ 

    Source code for osxphotos.iphoto

         
       
    - + - + diff --git a/docs/_modules/osxphotos/photoexporter.html b/docs/_modules/osxphotos/photoexporter.html index 514d2adbc..f3e105963 100644 --- a/docs/_modules/osxphotos/photoexporter.html +++ b/docs/_modules/osxphotos/photoexporter.html @@ -4,12 +4,12 @@ - - osxphotos.photoexporter - osxphotos 0.68.3 documentation + + osxphotos.photoexporter - osxphotos 0.68.6 documentation - + - + @@ -68,7 +68,7 @@ Light mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="feather-sun"> @@ -83,22 +83,63 @@ Dark mode + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon"> - - Auto light/dark mode + + Auto light/dark, in light mode - - - - - - - + stroke-width="1" stroke-linecap="round" stroke-linejoin="round" + class="icon-custom-derived-from-feather-sun-and-tabler-moon"> + + + + + + + + + + + + + + Auto light/dark, in dark mode + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -112,6 +153,8 @@
    Hide table of contents sidebar
    +Skip to content +
    @@ -123,13 +166,14 @@
    @@ -146,7 +190,7 @@