Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aa4a20bb5 | ||
|
|
109917321a | ||
|
|
7fb7a551a8 | ||
|
|
d3267cb0f0 | ||
|
|
1209d98e6e | ||
|
|
c90c241a36 | ||
|
|
206490cacc | ||
|
|
31212a1daa | ||
|
|
a46f8bd244 | ||
|
|
06b02b4a23 | ||
|
|
30c036a287 | ||
|
|
715d874306 | ||
|
|
e39424e906 | ||
|
|
b03670dc70 | ||
|
|
ce297ced0a | ||
|
|
c4788c0fd2 | ||
|
|
98e988cd6a | ||
|
|
b39f52cc35 | ||
|
|
d990670f72 | ||
|
|
97a0a65d8a | ||
|
|
f7ca3977a9 | ||
|
|
fbb05d6795 | ||
|
|
0982d0c548 | ||
|
|
007f0e0960 | ||
|
|
0f1866e39d | ||
|
|
da6c3b8440 | ||
|
|
10c6e91d80 | ||
|
|
0abdf62bef | ||
|
|
9efb99cbd4 | ||
|
|
0c293d0bf5 | ||
|
|
770d85759d | ||
|
|
80ee142c7f | ||
|
|
7f7e58a1bd | ||
|
|
6a6f1da746 | ||
|
|
db5c4a216f | ||
|
|
369fa553e8 | ||
|
|
29968269ff | ||
|
|
381141bc09 | ||
|
|
caa279e394 | ||
|
|
bd01e41714 | ||
|
|
32b23a09cb | ||
|
|
4857598324 | ||
|
|
b937874f66 | ||
|
|
69eb4b070c | ||
|
|
8c46b8e545 | ||
|
|
b6ab618154 | ||
|
|
7ab33f66d7 | ||
|
|
83d14ac191 | ||
|
|
aca2333477 | ||
|
|
15c75ff17c | ||
|
|
1e58e20b3c | ||
|
|
51ca4d30f3 | ||
|
|
15b331047c | ||
|
|
81f4a4c3ee | ||
|
|
227a3e2836 | ||
|
|
0688729785 | ||
|
|
8fa7f18ece | ||
|
|
99aff7396f | ||
|
|
c36610ab62 | ||
|
|
2aa80996bf | ||
|
|
de8f29a431 | ||
|
|
c383212822 | ||
|
|
5fc8c022ab | ||
|
|
8aa850e969 | ||
|
|
3dda7371ab | ||
|
|
4b3433fc20 | ||
|
|
106b258c6d | ||
|
|
8caee5a81b | ||
|
|
6788f318a2 | ||
|
|
1f7480a9b9 | ||
|
|
8b72374001 | ||
|
|
cf72a1f821 | ||
|
|
11266cd62b | ||
|
|
1a5bb0e36a | ||
|
|
a1a97eec13 | ||
|
|
eb0251b198 | ||
|
|
dbdeb069be | ||
|
|
1d5b51dd3d | ||
|
|
45392511e5 | ||
|
|
e76b2cfadc | ||
|
|
d6e0e340b7 | ||
|
|
d518ca5d5d | ||
|
|
a6cce9ef65 | ||
|
|
00481d3623 | ||
|
|
f5ed3d7518 | ||
|
|
c091a0b6c1 | ||
|
|
5f298709d7 | ||
|
|
9a9a1be165 | ||
|
|
89f82e92f0 | ||
|
|
5bd793ae64 | ||
|
|
de584e3dec | ||
|
|
a8586f911f | ||
|
|
00796f1c0a | ||
|
|
1b89f85a41 | ||
|
|
0472582870 | ||
|
|
830da7b3b4 | ||
|
|
5e7ab06458 | ||
|
|
b2b814954b | ||
|
|
8b9af7be67 | ||
|
|
a3aee63eab | ||
|
|
aeb6283b2b | ||
|
|
47e2454584 | ||
|
|
ee370f5dfb | ||
|
|
3e2076df12 | ||
|
|
2afab9e3b1 | ||
|
|
3c8d7e13b9 | ||
|
|
c3bd04f257 | ||
|
|
12fecec3de | ||
|
|
e4faf3779c | ||
|
|
53a61ed5aa | ||
|
|
debc001af9 | ||
|
|
025ee36086 | ||
|
|
d66cb6dc2b | ||
|
|
88e56bc0b9 | ||
|
|
327f19809e | ||
|
|
924a5f2f61 | ||
|
|
3557658b73 | ||
|
|
bb65765afa | ||
|
|
f8d8028631 |
@@ -261,7 +261,8 @@
|
||||
"contributions": [
|
||||
"bug",
|
||||
"ideas",
|
||||
"test"
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -404,6 +405,113 @@
|
||||
"contributions": [
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "drodner",
|
||||
"name": "drodner",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/10236892?v=4",
|
||||
"profile": "https://github.com/drodner",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "fmckeogh",
|
||||
"name": "Ferdia McKeogh",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8290136?v=4",
|
||||
"profile": "https://fmckeogh.github.io",
|
||||
"contributions": [
|
||||
"code",
|
||||
"bug"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "PetrochukM",
|
||||
"name": "Michael Petrochuk",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7424737?v=4",
|
||||
"profile": "https://wellsaidlabs.com",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "qkeddy",
|
||||
"name": "Quin Eddy",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9737814?v=4",
|
||||
"profile": "https://qkeddy.github.io/quin-eddy-development-portfolio/",
|
||||
"contributions": [
|
||||
"ideas",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "johnsturgeon",
|
||||
"name": "John Sturgeon",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/9746310?v=4",
|
||||
"profile": "https://github.com/johnsturgeon",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mave2k",
|
||||
"name": "mave2k",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8629837?v=4",
|
||||
"profile": "https://github.com/mave2k",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "djbeadle",
|
||||
"name": "Daniel Beadle",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6235378?v=4",
|
||||
"profile": "https://danielbeadle.net",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "eecue",
|
||||
"name": "Dave Bullock",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/532536?v=4",
|
||||
"profile": "http://eecue.com/",
|
||||
"contributions": [
|
||||
"ideas",
|
||||
"userTesting"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "pweaver",
|
||||
"name": "Pweaver",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/611620?v=4",
|
||||
"profile": "https://github.com/pweaver",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aa599",
|
||||
"name": "aa599",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/37746269?v=4",
|
||||
"profile": "https://github.com/aa599",
|
||||
"contributions": [
|
||||
"bug",
|
||||
"ideas"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "swduncan",
|
||||
"name": "Steve Duncan",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/2053195?v=4",
|
||||
"profile": "https://github.com/swduncan",
|
||||
"contributions": [
|
||||
"ideas"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 0.54.4
|
||||
current_version = 0.57.3
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
serialize = {major}.{minor}.{patch}
|
||||
|
||||
|
||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.py linguist-detectable=true
|
||||
*.js linguist-detectable=false
|
||||
*.html linguist-detectable=false
|
||||
*.css linguist-detectable=false
|
||||
5
.github/workflows/tests.yml
vendored
5
.github/workflows/tests.yml
vendored
@@ -13,11 +13,12 @@ jobs:
|
||||
python-version: ['3.9', '3.10', '3.11']
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: 'pip' # caching pip dependencies
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
|
||||
811
API_README.md
811
API_README.md
@@ -7,6 +7,7 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
|
||||
* [Example uses of the Python package](#example-uses-of-the-python-package)
|
||||
* [Package Interface](#package-interface)
|
||||
* [PhotosDB](#photosdb)
|
||||
* [QueryOptions](#queryoptions)
|
||||
* [PhotoInfo](#photoinfo)
|
||||
* [ExifInfo](#exifinfo)
|
||||
* [AlbumInfo](#albuminfo)
|
||||
@@ -32,124 +33,348 @@ In addition to a command line interface, OSXPhotos provides a access to a Python
|
||||
|
||||
## Example uses of the Python package
|
||||
|
||||
### Print filename, date created, title, and keywords for all photos in a library
|
||||
|
||||
```python
|
||||
""" Simple usage of the package """
|
||||
import os.path
|
||||
"""print filename, date created, title, and keywords for all photos in Photos library"""
|
||||
|
||||
import osxphotos
|
||||
|
||||
def main():
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.album_names)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
print(photosdb.albums_as_dict)
|
||||
|
||||
# find all photos with Keyword = Foo and containing John Smith
|
||||
photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"])
|
||||
|
||||
# find all photos that include Alice Smith but do not contain the keyword Bar
|
||||
photos = [p for p in photosdb.photos(persons=["Alice Smith"])
|
||||
if p not in photosdb.photos(keywords=["Bar"]) ]
|
||||
for p in photos:
|
||||
print(
|
||||
p.uuid,
|
||||
p.filename,
|
||||
p.original_filename,
|
||||
p.date,
|
||||
p.description,
|
||||
p.title,
|
||||
p.keywords,
|
||||
p.albums,
|
||||
p.persons,
|
||||
p.path,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
for photo in photosdb.photos():
|
||||
print(photo.original_filename, photo.date, photo.title, photo.keywords)
|
||||
|
||||
```
|
||||
|
||||
```python
|
||||
""" Export all photos to specified directory using album names as folders
|
||||
If file has been edited, also export the edited version,
|
||||
otherwise, export the original version
|
||||
This will result in duplicate photos if photo is in more than album """
|
||||
The primary interface to the Photos library is the [PhotosDB](#photosdb) object. The [PhotosDB](#photosdb) object provides access to the photos in the library via the [photos](#photosdbphotos) method and the [query](#photosdbquery). These methods returns a list of [PhotoInfo](#photoinfo) objects, one for each photo in the library. The [PhotoInfo](#photoinfo) object provides access to the metadata for each photo.
|
||||
|
||||
import os.path
|
||||
import pathlib
|
||||
import sys
|
||||
### Building simple command line tools
|
||||
|
||||
osxphotos provides several useful helper functions to make it easy to build simple command line tools. For example, the following code will print information about all photos in a library or a subset of photos filtered by one or more query options. This mirrors the `osxphotos query` command line tool. Tools built using these helper functions can be easily distributed as a single file and run via `osxphotos run script.py` so the user doesn't need to install python, any dependencies, or create a virtual environment.
|
||||
|
||||
Here's a simple example showing how to use the `query_command` decorator to implement a simple command line tool. The `query_command` decorator turns your function into a full-fledged [Click](https://palletsprojects.com/p/click/) command line app that can be run via `osxphotos run example.py` or `python example.py` if you have pip installed osxphotos. Your command will include all the query options available in `osxphotos query` as command line options as well as `--verbose` and other convenient options.
|
||||
|
||||
<!--[[[cog
|
||||
cog.out("```python\n")
|
||||
with open("examples/cli_example_1.py", "r") as f:
|
||||
cog.out(f.read())
|
||||
cog.out("```\n")
|
||||
]]]-->
|
||||
```python
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @query_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_1.py` or `python cli_example_1.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add all the query options available in `osxphotos query` as command line options
|
||||
as well as the following options:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will perform the query and pass the list of filtered PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
|
||||
For example, to run the command on only selected photos:
|
||||
|
||||
osxphotos run cli_example_1.py --selected
|
||||
|
||||
To run the command on all photos with the keyword "foo":
|
||||
|
||||
osxphotos run cli_example_1.py --keyword foo
|
||||
|
||||
For more advanced example, see `cli_example_2.py`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import query_command, verbose
|
||||
|
||||
|
||||
@query_command
|
||||
def example(photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample query command for osxphotos. Prints out the filename and date of each photo.
|
||||
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_1.py --help` or `python cli_example_1.py --help`
|
||||
"""
|
||||
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found {len(photos)} photo(s)")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
verbose(f"Processing {photo.original_filename}")
|
||||
print(f"{photo.original_filename} {photo.date}")
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
```
|
||||
<!--[[[end]]]-->
|
||||
|
||||
Here is a more advanced example that shows how to implement a script with a "dry run" and "resume" capability that preserves state between runs. Using the built-in helpers allows you to implement complex behavior in just a few lines of code.
|
||||
|
||||
<!--[[[cog
|
||||
cog.out("```python\n")
|
||||
with open("examples/cli_example_2.py", "r") as f:
|
||||
cog.out(f.read())
|
||||
cog.out("```\n")
|
||||
]]]-->
|
||||
```python
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @query_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_2.py` or `python cli_example_2.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add all the query options available in `osxphotos query` as command line options
|
||||
as well as the following options:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will perform the query and pass the list of filtered PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
|
||||
For example, to run the command on only selected photos:
|
||||
|
||||
osxphotos run cli_example_2.py --selected
|
||||
|
||||
To run the command on all photos with the keyword "foo":
|
||||
|
||||
osxphotos run cli_example_2.py --keyword foo
|
||||
|
||||
The following helper functions may be useful and can be imported from osxphotos.cli:
|
||||
|
||||
abort(message: str, exit_code: int = 1)
|
||||
Abort with error message and exit code
|
||||
echo(message: str)
|
||||
Print message to stdout using rich formatting
|
||||
echo_error(message: str)
|
||||
Print message to stderr using rich formatting
|
||||
logger: logging.Logger
|
||||
Python logger for osxphotos; for example, logger.debug("debug message")
|
||||
verbose(*args, level: int = 1)
|
||||
Print args to stdout if --verbose option is set
|
||||
query_command: decorator to create an osxphotos query command
|
||||
kvstore(name: str) -> SQLiteKVStore useful for storing state between runs
|
||||
|
||||
The verbose, echo, and echo_error functions use rich formatting to print messages to stdout and stderr.
|
||||
See https://github.com/Textualize/rich for more information on rich formatting.
|
||||
|
||||
In addition to standard rich formatting styles, the following styles will be defined
|
||||
(and can be changed using --theme):
|
||||
|
||||
[change]: something change
|
||||
[no_change]: indicate no change
|
||||
[count]: a count
|
||||
[error]: an error
|
||||
[filename]: a filename
|
||||
[filepath]: a filepath
|
||||
[num]: a number
|
||||
[time]: a time or date
|
||||
[tz]: a timezone
|
||||
[warning]: a warning
|
||||
[uuid]: a uuid
|
||||
|
||||
The tags should be closed with [/] to end the style. For example:
|
||||
|
||||
echo("[filename]foo[/] [time]bar[/]")
|
||||
|
||||
For simpler examples, see `cli_example_1.py`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import click
|
||||
from pathvalidate import is_valid_filepath, sanitize_filepath
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import (
|
||||
abort,
|
||||
echo,
|
||||
echo_error,
|
||||
kvstore,
|
||||
logger,
|
||||
query_command,
|
||||
verbose,
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("export_path", type=click.Path(exists=True))
|
||||
@query_command()
|
||||
@click.option(
|
||||
"--default-album",
|
||||
help="Default folder for photos with no album. Defaults to 'unfiled'",
|
||||
default="unfiled",
|
||||
"--resume",
|
||||
is_flag=True,
|
||||
help="Resume processing from last run, do not reprocess photos",
|
||||
)
|
||||
@click.option(
|
||||
"--library-path",
|
||||
help="Path to Photos library, default to last used library",
|
||||
default=None,
|
||||
"--dry-run", is_flag=True, help="Do a dry run, don't actually do anything"
|
||||
)
|
||||
def export(export_path, default_album, library_path):
|
||||
export_path = os.path.expanduser(export_path)
|
||||
library_path = os.path.expanduser(library_path) if library_path else None
|
||||
def example(resume, dry_run, photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample query command for osxphotos. Prints out the filename and date of each photo.
|
||||
|
||||
if library_path is not None:
|
||||
photosdb = osxphotos.PhotosDB(library_path)
|
||||
else:
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_2.py --help` or `python cli_example_2.py --help`
|
||||
|
||||
photos = photosdb.photos()
|
||||
The @query_command decorator returns a click.command so you can add additional options
|
||||
using standard click decorators. For example, the --resume and --dry-run options.
|
||||
For more information on click, see https://palletsprojects.com/p/click/.
|
||||
"""
|
||||
|
||||
for p in photos:
|
||||
if not p.ismissing:
|
||||
albums = p.albums
|
||||
if not albums:
|
||||
albums = [default_album]
|
||||
for album in albums:
|
||||
click.echo(f"exporting {p.filename} in album {album}")
|
||||
# abort will print the message to stderr and exit with the given exit code
|
||||
if not photos:
|
||||
abort("Nothing to do!", 1)
|
||||
|
||||
# make sure no invalid characters in destination path (could be in album name)
|
||||
album_name = sanitize_filepath(album, platform="auto")
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found [count]{len(photos)}[/] photos")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# create destination folder, if necessary, based on album name
|
||||
dest_dir = os.path.join(export_path, album_name)
|
||||
# the logger is a python logging.Logger object
|
||||
# debug messages will only be printed if --debug option is set
|
||||
logger.debug(f"{kwargs=}")
|
||||
|
||||
# verify path is a valid path
|
||||
if not is_valid_filepath(dest_dir, platform="auto"):
|
||||
sys.exit(f"Invalid filepath {dest_dir}")
|
||||
# kvstore() returns a SQLiteKVStore object for storing state between runs
|
||||
# this is basically a persistent dictionary that can be used to store state
|
||||
# see https://github.com/RhetTbull/sqlitekvstore for more information
|
||||
kv = kvstore("cli_example_2")
|
||||
verbose(f"Using key-value cache: {kv.path}")
|
||||
|
||||
# create destination dir if needed
|
||||
if not os.path.isdir(dest_dir):
|
||||
os.makedirs(dest_dir)
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
if resume and photo.uuid in kv:
|
||||
echo(
|
||||
f"Skipping processed photo [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
continue
|
||||
|
||||
# export the photo
|
||||
if p.hasadjustments:
|
||||
# export edited version
|
||||
exported = p.export(dest_dir, edited=True)
|
||||
edited_name = pathlib.Path(p.path_edited).name
|
||||
click.echo(f"Exported {edited_name} to {exported}")
|
||||
# export unedited version
|
||||
exported = p.export(dest_dir)
|
||||
click.echo(f"Exported {p.filename} to {exported}")
|
||||
else:
|
||||
click.echo(f"Skipping missing photo: {p.filename}")
|
||||
# store the uuid and current time in the kvstore
|
||||
# the key and value must be a type supported by SQLite: int, float, str, bytes, bool, None
|
||||
# if you need to store other values, you should serialize them to a string or bytes first
|
||||
# for example, using json.dumps() or pickle.dumps()
|
||||
kv[photo.uuid] = datetime.datetime.now().isoformat()
|
||||
echo(f"Processing [filename]{photo.original_filename}[/] [time]{photo.date}[/]")
|
||||
if not dry_run:
|
||||
# do something with the photo here
|
||||
echo(f"Doing something with [filename]{photo.original_filename}[/]")
|
||||
|
||||
# echo_error will print to stderr
|
||||
# if you add [warning] or [error], it will be formatted accordingly
|
||||
# and include an emoji to make the message stand out
|
||||
echo_error("[warning]This is a warning message!")
|
||||
echo_error("[error]This is an error message!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
export() # pylint: disable=no-value-for-parameter
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
```
|
||||
<!--[[[end]]]-->
|
||||
|
||||
In addition to the `query_command` decorator, you can also use the `selection_command` decorator to implement a command that operates on the current selection in Photos.
|
||||
|
||||
<!--[[[cog
|
||||
cog.out("```python\n")
|
||||
with open("examples/cli_example_3.py", "r") as f:
|
||||
cog.out(f.read())
|
||||
cog.out("```\n")
|
||||
]]]-->
|
||||
```python
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @selection_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_1.py` or `python cli_example_1.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add the following options to your command:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will get the selected photos and pass the list of PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import (
|
||||
selection_command,
|
||||
verbose,
|
||||
)
|
||||
|
||||
|
||||
@selection_command
|
||||
def example(photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample command for osxphotos. Prints out the filename and date of each photo
|
||||
currently selected in Photos.app.
|
||||
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_1.py --help` or `python cli_example_1.py --help`
|
||||
"""
|
||||
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found {len(photos)} photo(s)")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
verbose(f"Processing {photo.original_filename}")
|
||||
print(f"{photo.original_filename} {photo.date}")
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
```
|
||||
<!--[[[end]]]-->
|
||||
|
||||
## Package Interface
|
||||
|
||||
@@ -222,6 +447,132 @@ Returns a PhotosDB object.
|
||||
|
||||
**Note**: If you have a large library (e.g. many thousdands of photos), creating the PhotosDB object can take a long time (10s of seconds). See [Implementation Notes](#implementation-notes) for additional details.
|
||||
|
||||
#### <A name="photosdbphotos">`photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`</a>
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]],[from_date=datetime.datetime],[to_date=datetime.datetime])
|
||||
```
|
||||
|
||||
Returns a list of [PhotoInfo](#photoinfo) objects. Each PhotoInfo object represents a photo in the Photos Libary.
|
||||
|
||||
If called with no parameters, returns a list of every photo in the Photos library.
|
||||
|
||||
May be called with one or more of the following parameters to filter the list of photos returned:
|
||||
|
||||
```python
|
||||
photos = photosdb.photos(
|
||||
keywords = [],
|
||||
uuid = [],
|
||||
persons = [],
|
||||
albums = [],
|
||||
images = bool,
|
||||
movies = bool,
|
||||
from_date = datetime.datetime,
|
||||
to_date = datetime.datetime,
|
||||
intrash = bool,
|
||||
)
|
||||
```
|
||||
|
||||
* ```keywords```: list of one or more keywords. Returns only photos containing the keyword(s). If more than one keyword is provided finds photos matching any of the keywords (e.g. treated as "or")
|
||||
* ```uuid```: list of one or more uuids. Returns only photos whos UUID matches. **Note**: The UUID is the universally unique identifier that the Photos database uses to identify each photo. You shouldn't normally need to use this but it is a way to access a specific photo if you know the UUID. If more than more uuid is provided, returns photos that match any of the uuids (e.g. treated as "or")
|
||||
* ```persons```: list of one or more persons. Returns only photos containing the person(s). If more than one person provided, returns photos that match any of the persons (e.g. treated as "or")
|
||||
* ```albums```: list of one or more album names. Returns only photos contained in the album(s). If more than one album name is provided, returns photos contained in any of the albums (.e.g. treated as "or")
|
||||
* ```images```: bool; if True, returns photos/images; default is True
|
||||
* ```movies```: bool; if True, returns movies/videos; default is True
|
||||
* ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
|
||||
* ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
||||
* ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
|
||||
|
||||
See also [get_photo()](#getphoto) which is much faster for retrieving a single photo and [query](#photosdbquery) which provides much more flexibility in querying the database.
|
||||
|
||||
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
||||
|
||||
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")
|
||||
|
||||
```python
|
||||
photos=photosdb.photos(keywords=["wedding","birthday"],persons=["Juan Rodriguez"])
|
||||
```
|
||||
|
||||
Find all photos tagged with keyword "wedding":
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos = photosdb.photos(keywords=["wedding"])
|
||||
```
|
||||
|
||||
Find all photos of Maria Smith
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(persons=["Maria Smith"])
|
||||
```
|
||||
|
||||
Find all photos in album "Summer Vacation" or album "Ski Trip"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(albums=["Summer Vacation", "Ski Trip"])
|
||||
```
|
||||
|
||||
Find the single photo with uuid = "osMNIO5sQFGZTbj9WrydRB"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(uuid=["osMNIO5sQFGZTbj9WrydRB"])
|
||||
```
|
||||
|
||||
If you need to do more complicated searches, you can do this programmaticaly. For example, find photos with keyword = "Kids" but not in album "Vacation 2019"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos1 = photosdb.photos(albums=["Vacation 2019"])
|
||||
photos2 = photosdb.photos(keywords=["Kids"])
|
||||
photos3 = [p for p in photos2 if p not in photos1]
|
||||
```
|
||||
|
||||
To get only movies:
|
||||
|
||||
```python
|
||||
movies = photosdb.photos(images=False, movies=True)
|
||||
```
|
||||
|
||||
**Note** PhotosDB.photos() may return a different number of photos than Photos.app reports in the GUI. This is because photos() returns [hidden](#hidden) photos, [shared](#shared) photos, and for [burst](#burst) photos, all selected burst images even if non-selected burst images have not been deleted. Photos only reports 1 single photo for each set of burst images until you "finalize" the burst by selecting key photos and deleting the others using the "Make a selection" option.
|
||||
|
||||
For example, in my library, Photos says I have 19,386 photos and 474 movies. However, PhotosDB.photos() reports 25,002 photos. The difference is due to 5,609 shared photos and 7 hidden photos. (*Note* Shared photos only valid for Photos 5). Similarly, filtering for just movies returns 625 results. The difference between 625 and 474 reported by Photos is due to 151 shared movies.
|
||||
|
||||
```pycon
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
>>> photos = photosdb.photos()
|
||||
>>> len(photos)
|
||||
25002
|
||||
>>> shared = [p for p in photos if p.shared]
|
||||
>>> len(shared)
|
||||
5609
|
||||
>>> not_shared = [p for p in photos if not p.shared]
|
||||
>>> len(not_shared)
|
||||
19393
|
||||
>>> hidden = [p for p in photos if p.hidden]
|
||||
>>> len(hidden)
|
||||
7
|
||||
>>> movies = photosdb.photos(movies=True, images=False)
|
||||
>>> len(movies)
|
||||
625
|
||||
>>> shared_movies = [m for m in movies if m.shared]
|
||||
>>> len(shared_movies)
|
||||
151
|
||||
>>>
|
||||
```
|
||||
|
||||
#### <a name="getphoto">`get_photo(uuid)`</A>
|
||||
|
||||
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
||||
|
||||
#### <A name="photosdbquery">`query(options: QueryOptions) -> List[PhotoInfo]:`</a>
|
||||
|
||||
Returns a list of [PhotoInfo](#photoinfo) objects matching the query options. This is preferred method of querying the photos database. See [QueryOptions](#queryoptions) for details on the options available.
|
||||
|
||||
#### `keywords`
|
||||
|
||||
```python
|
||||
@@ -238,7 +589,7 @@ Returns a list of the keywords found in the Photos library
|
||||
albums = photosdb.album_info
|
||||
```
|
||||
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums) and [burst_album_info](#burst_album_info).
|
||||
Returns a list of [AlbumInfo](#albuminfo) objects representing albums in the database or empty list if there are no albums. See also [albums](#albums) and [burst_album_info](#burst_album_info).
|
||||
|
||||
#### `albums`
|
||||
|
||||
@@ -278,7 +629,7 @@ Returns the [MomentInfo](#momentinfo) object for the photo or `None` if the pho
|
||||
folders = photosdb.folder_info
|
||||
```
|
||||
|
||||
Returns a list of [FolderInfo](#FolderInfo) objects representing top level folders in the database or empty list if there are no folders. See also [folders](#folders).
|
||||
Returns a list of [FolderInfo](#folderinfo) objects representing top level folders in the database or empty list if there are no folders. See also [folders](#folders).
|
||||
|
||||
**Note**: Currently folder_info is only implemented for Photos 5 (Catalina); will return empty list and output warning if called on earlier database versions.
|
||||
|
||||
@@ -402,6 +753,15 @@ photosdb.db_version
|
||||
|
||||
Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested.
|
||||
|
||||
#### `photos_version`
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photosdb.photos_version
|
||||
```
|
||||
|
||||
Returns the version number as int for version of Photos that created the library, e.g. 2, 3, 4, 5...
|
||||
|
||||
#### `get_db_connection()`
|
||||
|
||||
Returns tuple of (connection, cursor) for the working copy of the Photos database. This is useful for debugging or prototyping new features.
|
||||
@@ -421,132 +781,110 @@ for row in results:
|
||||
conn.close()
|
||||
```
|
||||
|
||||
#### <A name="photos">`photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=True, from_date=None, to_date=None, intrash=False)`</a>
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]],[from_date=datetime.datetime],[to_date=datetime.datetime])
|
||||
```
|
||||
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects. Each PhotoInfo object represents a photo in the Photos Libary.
|
||||
|
||||
If called with no parameters, returns a list of every photo in the Photos library.
|
||||
|
||||
May be called with one or more of the following parameters:
|
||||
|
||||
```python
|
||||
photos = photosdb.photos(
|
||||
keywords = [],
|
||||
uuid = [],
|
||||
persons = [],
|
||||
albums = [],
|
||||
images = bool,
|
||||
movies = bool,
|
||||
from_date = datetime.datetime,
|
||||
to_date = datetime.datetime,
|
||||
intrash = bool,
|
||||
)
|
||||
```
|
||||
|
||||
* ```keywords```: list of one or more keywords. Returns only photos containing the keyword(s). If more than one keyword is provided finds photos matching any of the keywords (e.g. treated as "or")
|
||||
* ```uuid```: list of one or more uuids. Returns only photos whos UUID matches. **Note**: The UUID is the universally unique identifier that the Photos database uses to identify each photo. You shouldn't normally need to use this but it is a way to access a specific photo if you know the UUID. If more than more uuid is provided, returns photos that match any of the uuids (e.g. treated as "or")
|
||||
* ```persons```: list of one or more persons. Returns only photos containing the person(s). If more than one person provided, returns photos that match any of the persons (e.g. treated as "or")
|
||||
* ```albums```: list of one or more album names. Returns only photos contained in the album(s). If more than one album name is provided, returns photos contained in any of the albums (.e.g. treated as "or")
|
||||
* ```images```: bool; if True, returns photos/images; default is True
|
||||
* ```movies```: bool; if True, returns movies/videos; default is True
|
||||
* ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None
|
||||
* ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None
|
||||
* ```intrash```: if True, finds only photos in the "Recently Deleted" or trash folder, if False does not find any photos in the trash; default is False
|
||||
|
||||
See also [get_photo()](#getphoto) which is much faster for retrieving a single photo.
|
||||
|
||||
If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g.
|
||||
|
||||
Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez")
|
||||
|
||||
```python
|
||||
photos=photosdb.photos(keywords=["wedding","birthday"],persons=["Juan Rodriguez"])
|
||||
```
|
||||
|
||||
Find all photos tagged with keyword "wedding":
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos = photosdb.photos(keywords=["wedding"])
|
||||
```
|
||||
|
||||
Find all photos of Maria Smith
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(persons=["Maria Smith"])
|
||||
```
|
||||
|
||||
Find all photos in album "Summer Vacation" or album "Ski Trip"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(albums=["Summer Vacation", "Ski Trip"])
|
||||
```
|
||||
|
||||
Find the single photo with uuid = "osMNIO5sQFGZTbj9WrydRB"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos=photosdb.photos(uuid=["osMNIO5sQFGZTbj9WrydRB"])
|
||||
```
|
||||
|
||||
If you need to do more complicated searches, you can do this programmaticaly. For example, find photos with keyword = "Kids" but not in album "Vacation 2019"
|
||||
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photos1 = photosdb.photos(albums=["Vacation 2019"])
|
||||
photos2 = photosdb.photos(keywords=["Kids"])
|
||||
photos3 = [p for p in photos2 if p not in photos1]
|
||||
```
|
||||
|
||||
To get only movies:
|
||||
|
||||
```python
|
||||
movies = photosdb.photos(images=False, movies=True)
|
||||
```
|
||||
|
||||
**Note** PhotosDB.photos() may return a different number of photos than Photos.app reports in the GUI. This is because photos() returns [hidden](#hidden) photos, [shared](#shared) photos, and for [burst](#burst) photos, all selected burst images even if non-selected burst images have not been deleted. Photos only reports 1 single photo for each set of burst images until you "finalize" the burst by selecting key photos and deleting the others using the "Make a selection" option.
|
||||
|
||||
For example, in my library, Photos says I have 19,386 photos and 474 movies. However, PhotosDB.photos() reports 25,002 photos. The difference is due to 5,609 shared photos and 7 hidden photos. (*Note* Shared photos only valid for Photos 5). Similarly, filtering for just movies returns 625 results. The difference between 625 and 474 reported by Photos is due to 151 shared movies.
|
||||
|
||||
```pycon
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
>>> photos = photosdb.photos()
|
||||
>>> len(photos)
|
||||
25002
|
||||
>>> shared = [p for p in photos if p.shared]
|
||||
>>> len(shared)
|
||||
5609
|
||||
>>> not_shared = [p for p in photos if not p.shared]
|
||||
>>> len(not_shared)
|
||||
19393
|
||||
>>> hidden = [p for p in photos if p.hidden]
|
||||
>>> len(hidden)
|
||||
7
|
||||
>>> movies = photosdb.photos(movies=True, images=False)
|
||||
>>> len(movies)
|
||||
625
|
||||
>>> shared_movies = [m for m in movies if m.shared]
|
||||
>>> len(shared_movies)
|
||||
151
|
||||
>>>
|
||||
```
|
||||
|
||||
#### <a name="getphoto">`get_photo(uuid)`</A>
|
||||
|
||||
Returns a single PhotoInfo instance for photo with UUID matching `uuid` or None if no photo is found matching `uuid`. If you know the UUID of a photo, `get_photo()` is much faster than `photos`. See also [photos()](#photos).
|
||||
|
||||
#### `execute(sql)`
|
||||
|
||||
Execute sql statement against the Photos database and return a sqlite cursor with the results.
|
||||
|
||||
### QueryOptions
|
||||
|
||||
QueryOptions class for [PhotosDB.query()](#photosdbquery)
|
||||
|
||||
#### Attributes
|
||||
|
||||
See [queryoptions.py](https://github.com/RhetTbull/osxphotos/blob/master/osxphotos/queryoptions.py) for typing information.
|
||||
|
||||
* `added_after`: search for photos added after a given date
|
||||
* `added_before`: search for photos added before a given date
|
||||
* `added_in_last`: search for photos added in last X datetime.timedelta
|
||||
* `album`: list of album names to search for
|
||||
* `burst_photos`: include all associated burst photos for photos in query results
|
||||
* `burst`: search for burst photos
|
||||
* `cloudasset`: search for photos that are managed by iCloud
|
||||
* `deleted_only`: search only for deleted photos
|
||||
* `deleted`: also include deleted photos
|
||||
* `description`: list of descriptions to search for
|
||||
* `duplicate`: search for duplicate photos
|
||||
* `edited`: search for edited photos
|
||||
* `exif`: search for photos with EXIF tags that matches the given data
|
||||
* `external_edit`: search for photos edited in external apps
|
||||
* `favorite`: search for favorite photos
|
||||
* `folder`: list of folder names to search for
|
||||
* `from_date`: search for photos taken on or after this date
|
||||
* `function`: list of query functions to evaluate
|
||||
* `has_comment`: search for photos with comments
|
||||
* `has_likes`: search for shared photos with likes
|
||||
* `has_raw`: search for photos with associated raw files
|
||||
* `hdr`: search for HDR photos
|
||||
* `hidden`: search for hidden photos
|
||||
* `ignore_case`: ignore case when searching
|
||||
* `in_album`: search for photos in an album
|
||||
* `incloud`: search for cloud assets that are synched to iCloud
|
||||
* `is_reference`: search for photos stored by reference (that is, they are not managed by Photos)
|
||||
* `keyword`: list of keywords to search for
|
||||
* `label`: list of labels to search for
|
||||
* `live`: search for live photos
|
||||
* `location`: search for photos with a location
|
||||
* `max_size`: maximum size of photos to search for
|
||||
* `min_size`: minimum size of photos to search for
|
||||
* `missing_bursts`: for burst photos, also include burst photos that are missing
|
||||
* `missing`: search for missing photos
|
||||
* `movies`: search for movies
|
||||
* `name`: list of names to search for
|
||||
* `no_comment`: search for photos with no comments
|
||||
* `no_description`: search for photos with no description
|
||||
* `no_likes`: search for shared photos with no likes
|
||||
* `no_location`: search for photos with no location
|
||||
* `no_keyword`: search for photos with no keywords
|
||||
* `no_place`: search for photos with no place
|
||||
* `no_title`: search for photos with no title
|
||||
* `not_burst`: search for non-burst photos
|
||||
* `not_cloudasset`: search for photos that are not managed by iCloud
|
||||
* `not_edited`: search for photos that have not been edited
|
||||
* `not_favorite`: search for non-favorite photos
|
||||
* `not_hdr`: search for non-HDR photos
|
||||
* `not_hidden`: search for non-hidden photos
|
||||
* `not_in_album`: search for photos not in an album
|
||||
* `not_incloud`: search for cloud asset photos that are not yet synched to iCloud
|
||||
* `not_live`: search for non-live photos
|
||||
* `not_missing`: search for non-missing photos
|
||||
* `not_panorama`: search for non-panorama photos
|
||||
* `not_portrait`: search for non-portrait photos
|
||||
* `not_reference`: search for photos not stored by reference (that is, they are managed by Photos)
|
||||
* `not_screenshot`: search for non-screenshot photos
|
||||
* `not_selfie`: search for non-selfie photos
|
||||
* `not_shared`: search for non-shared photos
|
||||
* `not_slow_mo`: search for non-slow-mo photos
|
||||
* `not_time_lapse`: search for non-time-lapse photos
|
||||
* `panorama`: search for panorama photos
|
||||
* `person`: list of person names to search for
|
||||
* `photos`: search for photos
|
||||
* `place`: list of place names to search for
|
||||
* `portrait`: search for portrait photos
|
||||
* `query_eval`: list of query expressions to evaluate
|
||||
* `regex`: list of regular expressions to search for
|
||||
* `screenshot`: search for screenshot photos
|
||||
* `selected`: search for selected photos
|
||||
* `selfie`: search for selfie photos
|
||||
* `shared`: search for shared photos
|
||||
* `slow_mo`: search for slow-mo photos
|
||||
* `time_lapse`: search for time-lapse photos
|
||||
* `title`: list of titles to search for
|
||||
* `to_date`: search for photos taken on or before this date
|
||||
* `uti`: list of UTIs to search for
|
||||
* `uuid`: list of uuids to search for
|
||||
* `year`: search for photos taken in a given year
|
||||
|
||||
```python
|
||||
"""Find all screenshots taken in 2019"""
|
||||
import osxphotos
|
||||
|
||||
if __name__ == "__main__":
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
results = photosdb.query(osxphotos.QueryOptions(screenshot=True, year=[2019]))
|
||||
for photo in results:
|
||||
print(photo.original_filename, photo.date)
|
||||
```
|
||||
|
||||
### PhotoInfo
|
||||
|
||||
PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object represents a single photo in the Photos library.
|
||||
@@ -593,7 +931,7 @@ Returns a list of albums the photo is contained in. See also [album_info](#album
|
||||
|
||||
#### `album_info`
|
||||
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing the albums the photo is contained in or empty list of the photo is not in any albums. See also [albums](#albums).
|
||||
Returns a list of [AlbumInfo](#albuminfo) objects representing the albums the photo is contained in or empty list of the photo is not in any albums. See also [albums](#albums).
|
||||
|
||||
#### `import_info`
|
||||
|
||||
@@ -729,7 +1067,7 @@ Returns latitude and longitude as a tuple of floats (latitude, longitude). If l
|
||||
|
||||
#### `place`
|
||||
|
||||
Returns a [PlaceInfo](#PlaceInfo) object with reverse geolocation data or None if there is the photo has no reverse geolocation information.
|
||||
Returns a [PlaceInfo](#placeinfo) object with reverse geolocation data or None if there is the photo has no reverse geolocation information.
|
||||
|
||||
#### `shared`
|
||||
|
||||
@@ -842,7 +1180,7 @@ If a burst photo which has unselected burst images (e.g. the burst images are in
|
||||
|
||||
#### `burst_album_info`
|
||||
|
||||
If photo is non-selected burst photo, teturns a list of [AlbumInfo](#AlbumInfo) objects representing the albums any other photos in the same burst set are contained in. Otherwise, returns `PhotoInfo.album_info`. See also [burst_albums](#burst_albums) and [album_info](#album_info).
|
||||
If photo is non-selected burst photo, teturns a list of [AlbumInfo](#albuminfo) objects representing the albums any other photos in the same burst set are contained in. Otherwise, returns `PhotoInfo.album_info`. See also [burst_albums](#burst_albums) and [album_info](#album_info).
|
||||
|
||||
#### `live_photo`
|
||||
|
||||
@@ -974,6 +1312,18 @@ Returns a [ScoreInfo](#scoreinfo) data class object which provides access to the
|
||||
|
||||
Returns list of PhotoInfo objects for *possible* duplicates or empty list if no matching duplicates. Photos are considered possible duplicates if the photo's original file size, date created, height, and width match another those of another photo. This does not do a byte-for-byte comparison or compute a hash which makes it fast and allows for identification of possible duplicates even if originals are not downloaded from iCloud. The signature-based approach should be robust enough to match duplicates created either through the "duplicate photo" menu item or imported twice into the library but you should not rely on this 100% for identification of all duplicates.
|
||||
|
||||
#### `cloud_guid`
|
||||
|
||||
For photos in iCloud, returns the cloud GUID for the photo. This is the unique identifier for the photo in iCloud. For photos not in iCloud, returns None.
|
||||
|
||||
#### `cloud_owner_hashed_id`
|
||||
|
||||
For shared photos, returns the hashed ID of the owner of the shared photo. For photos not shared, returns None.
|
||||
|
||||
#### `fingerprint`
|
||||
|
||||
Returns a unique fingerprint for the original photo file. This is a hash of the original photo file and is useful for finding duplicates or correlating photos across multiple libraries.
|
||||
|
||||
#### `hexdigest`
|
||||
|
||||
Returns a unique digest of the photo's properties and metadata; useful for detecting changes in any property/metadata of the photo.
|
||||
@@ -1125,7 +1475,7 @@ Returns the title or name of the album.
|
||||
|
||||
#### <a name="albumphotos">`photos`</a>
|
||||
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
|
||||
Returns a list of [PhotoInfo](#photoinfo) objects representing each photo contained in the album sorted in the same order as in Photos. (e.g. if photos were manually sorted in the Photos albums, photos returned by `photos` will be in same order as they appear in the Photos album)
|
||||
|
||||
#### `creation_date`
|
||||
|
||||
@@ -1141,7 +1491,7 @@ Returns the date of latest photo in the album as a timezone aware datetime.datet
|
||||
|
||||
#### `folder_list`
|
||||
|
||||
Returns a hierarchical list of [FolderInfo](#FolderInfo) objects representing the folders the album is contained in. For example, if album "AlbumInFolder" is in SubFolder2 of Folder1 as illustrated below, would return a list of `FolderInfo` objects representing ["Folder1", "SubFolder2"]
|
||||
Returns a hierarchical list of [FolderInfo](#folderinfo) objects representing the folders the album is contained in. For example, if album "AlbumInFolder" is in SubFolder2 of Folder1 as illustrated below, would return a list of `FolderInfo` objects representing ["Folder1", "SubFolder2"]
|
||||
|
||||
```txt
|
||||
Photos Library
|
||||
@@ -1165,7 +1515,7 @@ Photos Library
|
||||
|
||||
#### `parent`
|
||||
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||
Returns a [FolderInfo](#folderinfo) object representing the albums parent folder or `None` if album is not a in a folder.
|
||||
|
||||
#### `owner`
|
||||
|
||||
@@ -1185,7 +1535,7 @@ Returns the universally unique identifier (uuid) of the import session. This is
|
||||
|
||||
#### <a name="importphotos">`photos`</a>
|
||||
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the import session.
|
||||
Returns a list of [PhotoInfo](#photoinfo) objects representing each photo contained in the import session.
|
||||
|
||||
#### `creation_date`
|
||||
|
||||
@@ -1215,7 +1565,7 @@ Returns the title or name of the project.
|
||||
|
||||
#### <a name="projectphotos">`photos`</a>
|
||||
|
||||
Returns a list of [PhotoInfo](#PhotoInfo) objects representing each photo contained in the project.
|
||||
Returns a list of [PhotoInfo](#photoinfo) objects representing each photo contained in the project.
|
||||
|
||||
#### `creation_date`
|
||||
|
||||
@@ -1279,21 +1629,21 @@ Returns the title or name of the folder.
|
||||
|
||||
#### `album_info`
|
||||
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects representing each album contained in the folder.
|
||||
Returns a list of [AlbumInfo](#albuminfo) objects representing each album contained in the folder.
|
||||
|
||||
#### `album_info_shared`
|
||||
|
||||
Returns a list of [AlbumInfo](#AlbumInfo) objects for each shared album in the photos database.
|
||||
Returns a list of [AlbumInfo](#albuminfo) objects for each shared album in the photos database.
|
||||
|
||||
**Note**: Only valid for Photos 5+; on Photos <= 4, prints warning and returns empty list.
|
||||
|
||||
#### `subfolders`
|
||||
|
||||
Returns a list of [FolderInfo](#FolderInfo) objects representing the sub-folders of the folder.
|
||||
Returns a list of [FolderInfo](#folderinfo) objects representing the sub-folders of the folder.
|
||||
|
||||
#### `parent`
|
||||
|
||||
Returns a [FolderInfo](#FolderInfo) object representing the folder's parent folder or `None` if album is not a in a folder.
|
||||
Returns a [FolderInfo](#folderinfo) object representing the folder's parent folder or `None` if album is not a in a folder.
|
||||
|
||||
#### `sort_order`
|
||||
|
||||
@@ -1568,6 +1918,14 @@ Returns a list of PhotoInfo objects representing all photos the person appears i
|
||||
|
||||
Returns a list of [FaceInfo](#faceinfo) objects associated with this person sorted by quality score. Highest quality face is result[0] and lowest quality face is result[n].
|
||||
|
||||
#### <a name="personfavorite">`favorite`</a>
|
||||
|
||||
Returns True if Person has been marked as a favorite in Photos, otherwise False. Available on Photos 5+ only; on Photos <=4, returns False.
|
||||
|
||||
#### <a name="personsortorder">`sort_order`</a>
|
||||
|
||||
Returns the sort order for the person in the Photos People view. Available on Photos 5+ only; on Photos <= 4, returns None.
|
||||
|
||||
#### `json()`
|
||||
|
||||
Returns a json string representation of the PersonInfo instance.
|
||||
@@ -1995,6 +2353,7 @@ cog.out(get_template_field_table())
|
||||
|{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'|
|
||||
|{shortuuid}|A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'|
|
||||
|{id}|A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc. |
|
||||
|{counter}|A sequential counter, starting at 0, that increments each time it is evaluated.To start counting at a value other than 0, append append '(starting_value)' to the field name.For example, to start counting at 1 instead of 0: '{counter(1)}'.May be formatted using a python string format code.For example, to format as a 5-digit integer and pad with zeros, use '{counter:05d(1)}'which results in 00001, 00002, 00003...etc.You may also specify a stop value which causes the counter to reset to the starting valuewhen the stop value is reached and a step size which causes the counter to increment bythe specified value instead of 1. Use the format '{counter(start,stop,step)}' where start,stop, and step are integers. For example, to count from 1 to 10 by 2, use '{counter(1,11,2)}'.Note that the counter stops counting when the stop value is reached and does not return thestop value. Start, stop, and step are optional and may be omitted. For example, to countfrom 0 by 2s, use '{counter(,,2)}'.You may create an arbitrary number of counters by appending a unique name to the field namepreceded by a period: '{counter.a}', '{counter.b}', etc. Each counter will have its own stateand will start at 0 and increment by 1 unless otherwise specified. Note: {counter} is not suitable for use with 'export' and '--update' as the counter associated with a photo may change between export sessions. See also {id}.|
|
||||
|{album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album_seq}_{original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.|
|
||||
|{folder_album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"'. To start counting at a value other than 0, append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'. |
|
||||
|{comma}|A comma: ','|
|
||||
@@ -2012,7 +2371,7 @@ cog.out(get_template_field_table())
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|A carriage return + line feed: '\r\n'|
|
||||
|{tab}|:A tab: '\t'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.54.4'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.57.3'|
|
||||
|{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|
|
||||
@@ -2237,7 +2596,7 @@ def main():
|
||||
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.album_names)
|
||||
print(photosdb.albums)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
|
||||
1115
CHANGELOG.md
1115
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
327
README.md
327
README.md
@@ -7,7 +7,7 @@
|
||||
[](https://pepy.tech/project/osxphotos)
|
||||
[](https://www.reddit.com/r/osxphotos/)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors)
|
||||
[](#contributors)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos.app library on macOS. You can query the Photos library database — for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc. You can also easily export both the original and edited photos.
|
||||
@@ -37,17 +37,15 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through mac
|
||||
|
||||
| macOS Version | macOS name | Photos.app version |
|
||||
| ----------------- |------------|:-------------------|
|
||||
| 13.0 | Ventura | 8.0 ? * |
|
||||
| 12.0 - 12.4 | Monterey | 7.0 ✅ ** |
|
||||
| 13.0 | Ventura | 8.0 ✅ * |
|
||||
| 12.0 - 12.6 | Monterey | 7.0 ✅ * |
|
||||
| 10.16, 11.0-11.4 | Big Sur | 6.0 ✅ |
|
||||
| 10.15.1 - 10.15.7 | Catalina | 5.0 ✅ |
|
||||
| 10.14.5, 10.14.6 | Mojave | 4.0 ✅ |
|
||||
| 10.13.6 | High Sierra| 3.0 ✅ |
|
||||
| 10.12.6 | Sierra | 2.0 ✅ |
|
||||
|
||||
\* Basic functionality has been tested on a Photos library created with the developer preview of macOS Ventura (13.0). I do not have access to a Mac running Ventura beta to do further testing.
|
||||
|
||||
\*\* Some features may not be fully supported on Monterey. Notably, `--use-photokit` and `--download-missing` may or may not work depending on your configuration; this is a known issue that will be fixed if I can find a solution. Many users successfully use OSXPhotos on Monterey without problem.
|
||||
\* Some features may not be fully supported on Monterey and Ventura. Notably, `--use-photokit` and `--download-missing` may or may not work depending on your configuration; this is a known issue that will be fixed if I can find a solution. Many users successfully use OSXPhotos on Monterey without problem.
|
||||
|
||||
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.
|
||||
|
||||
@@ -64,6 +62,7 @@ If you aren't familiar with installing python applications, I recommend you inst
|
||||
* Open `Terminal` (search for `Terminal` in Spotlight or look in `Applications/Utilities`)
|
||||
* Install `homebrew` according to instructions at [https://brew.sh/](https://brew.sh/)
|
||||
* Type the following into Terminal: `brew install pipx`
|
||||
* Ensure that pipx installed packages are accessible in your PATH by typing: `pipx ensurepath`
|
||||
* Then type this: `pipx install osxphotos`
|
||||
* Now you should be able to run `osxphotos` by typing: `osxphotos`
|
||||
|
||||
@@ -113,56 +112,68 @@ If you have questions, would like to show off projects created with OSXPhotos, o
|
||||
|
||||
This package will install a command line utility called `osxphotos` that allows you to query the Photos database. Alternatively, you can also run the command line utility like this: `python3 -m osxphotos`
|
||||
|
||||
<!--[[[cog
|
||||
from osxphotos.cli import cli_main
|
||||
from click.testing import CliRunner
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(cli_main, ["--help"])
|
||||
help = result.output.replace("Usage: cli-main", "Usage: osxphotos")
|
||||
cog.out(
|
||||
"```\n{}\n```".format(help)
|
||||
)
|
||||
]]] -->
|
||||
```
|
||||
> osxphotos
|
||||
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
osxphotos: query and export your Photos library
|
||||
osxphotos: the multi-tool for your Photos library
|
||||
|
||||
Options:
|
||||
--db PHOTOS_LIBRARY_PATH Specify Photos database path. Path to Photos
|
||||
library/database can be specified using either
|
||||
--db or directly as PHOTOS_LIBRARY positional
|
||||
argument. If neither --db or PHOTOS_LIBRARY
|
||||
provided, will attempt to find the library to use
|
||||
in the following order: 1. last opened library, 2.
|
||||
system library, 3. ~/Pictures/Photos
|
||||
Library.photoslibrary
|
||||
--json Print output in JSON format.
|
||||
-v, --version Show the version and exit.
|
||||
-h, --help Show this message and exit.
|
||||
-v, --version Show the version and exit.
|
||||
--library, --db PHOTOS_LIBRARY_PATH
|
||||
Specify path to Photos library. If not
|
||||
provided, will attempt to find the library to
|
||||
use in the following order: 1. last opened
|
||||
library, 2. system library, 3.
|
||||
~/Pictures/Photos Library.photoslibrary
|
||||
--json Print output in JSON format.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
about Print information about osxphotos including license.
|
||||
albums Print out albums found in the Photos library.
|
||||
diff Compare two Photos databases and print out differences
|
||||
docs Open osxphotos documentation in your browser.
|
||||
dump Print list of all photos & associated info from the Photos...
|
||||
exiftool Run exiftool on previously exported files to update metadata.
|
||||
export Export photos from the Photos database.
|
||||
exportdb Utilities for working with the osxphotos export database
|
||||
help Print help; for help on commands: help <command>.
|
||||
import Import photos and videos into Photos.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
inspect Interactively inspect photos selected in Photos.
|
||||
install Install Python packages into the same environment as osxphotos
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
orphans Find orphaned photos in a Photos library
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options; if...
|
||||
repl Run interactive osxphotos REPL shell (useful for debugging,...
|
||||
run Run a python file using same environment as osxphotos.
|
||||
snap Create snapshot of Photos database to use with diff command
|
||||
theme Manage osxphotos color themes.
|
||||
timewarp Adjust date/time/timezone of photos in Apple Photos.
|
||||
tutorial Display osxphotos tutorial.
|
||||
uninstall Uninstall Python packages from the osxphotos environment
|
||||
uuid Print out unique IDs (UUID) of photos selected in Photos
|
||||
version Check for new version of osxphotos.
|
||||
about Print information about osxphotos including license.
|
||||
add-locations Add missing location data to photos in Photos.app using...
|
||||
albums Print out albums found in the Photos library.
|
||||
diff Compare two Photos databases and print out differences
|
||||
docs Open osxphotos documentation in your browser.
|
||||
dump Print list of all photos & associated info from the Photos...
|
||||
exiftool Run exiftool on previously exported files to update metadata.
|
||||
export Export photos from the Photos database.
|
||||
exportdb Utilities for working with the osxphotos export database
|
||||
help Print help; for help on commands: help <command>.
|
||||
import Import photos and videos into Photos.
|
||||
info Print out descriptive info of the Photos library database.
|
||||
inspect Interactively inspect photos selected in Photos.
|
||||
install Install Python packages into the same environment as...
|
||||
keywords Print out keywords found in the Photos library.
|
||||
labels Print out image classification labels found in the Photos...
|
||||
list Print list of Photos libraries found on the system.
|
||||
orphans Find orphaned photos in a Photos library
|
||||
persons Print out persons (faces) found in the Photos library.
|
||||
places Print out places found in the Photos library.
|
||||
query Query the Photos database using 1 or more search options;...
|
||||
repl Run interactive osxphotos REPL shell (useful for...
|
||||
run Run a python file using same environment as osxphotos.
|
||||
show Show photo, album, or folder in Photos from UUID_OR_NAME
|
||||
snap Create snapshot of Photos database to use with diff command
|
||||
sync Sync metadata and albums between Photos libraries.
|
||||
theme Manage osxphotos color themes.
|
||||
timewarp Adjust date/time/timezone of photos in Apple Photos.
|
||||
tutorial Display osxphotos tutorial.
|
||||
uninstall Uninstall Python packages from the osxphotos environment
|
||||
uuid Print out unique IDs (UUID) of photos selected in Photos
|
||||
version Check for new version of osxphotos.
|
||||
|
||||
```
|
||||
<!--[[[end]]] -->
|
||||
|
||||
To get help on a specific command, use `osxphotos help command_name`, for example, `osxphotos help export` to get help on the `export` command.
|
||||
|
||||
@@ -653,24 +664,35 @@ 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.
|
||||
than one different option is provided, they are treated as "AND" (e.g. search
|
||||
for photos matching all options). If the same query option is provided
|
||||
multiple times, they are treated as "OR" (e.g. search for photos matching any
|
||||
of the options). If no query options are provided, all photos will be
|
||||
exported.
|
||||
|
||||
For example, adding the query options:
|
||||
|
||||
--person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will export all photos with either person of ("John Doe" OR "Jane Doe") AND
|
||||
keyword of "vacation"
|
||||
|
||||
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_LIBRARY_PATH Specify Photos database path. Path to Photos
|
||||
library/database can be specified using either
|
||||
--db or directly as PHOTOS_LIBRARY positional
|
||||
argument. If neither --db or PHOTOS_LIBRARY
|
||||
--library, --db PHOTOS_LIBRARY_PATH
|
||||
Specify path to Photos library. If not
|
||||
provided, will attempt to find the library to
|
||||
use in the following order: 1. last opened
|
||||
library, 2. system library, 3.
|
||||
~/Pictures/Photos Library.photoslibrary
|
||||
-V, --verbose Print verbose output.
|
||||
-V, --verbose Print verbose output; may be specified
|
||||
multiple times for more verbose output.
|
||||
--timestamp Add time stamp to verbose output
|
||||
--no-progress Do not display progress bar during export.
|
||||
--keyword KEYWORD Search for photos with keyword KEYWORD. If
|
||||
@@ -695,8 +717,9 @@ Options:
|
||||
--uuid UUID Search for photos with UUID(s). May be
|
||||
repeated to include multiple UUIDs.
|
||||
--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.
|
||||
FILE. Format is a single UUID per line. Lines
|
||||
preceded with # are ignored. If FILE is '-',
|
||||
read UUIDs from stdin.
|
||||
--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.
|
||||
@@ -710,7 +733,7 @@ Options:
|
||||
--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 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
|
||||
@@ -718,15 +741,16 @@ Options:
|
||||
-i, --ignore-case Case insensitive search for title,
|
||||
description, place, keyword, person, or album.
|
||||
--edited Search for photos that have been edited.
|
||||
--not-edited Search for photos that have not been edited.
|
||||
--external-edit Search for photos edited in external editor.
|
||||
--favorite Search for photos marked favorite.
|
||||
--not-favorite Search for photos not marked favorite.
|
||||
--hidden Search for photos marked hidden.
|
||||
--not-hidden Search for photos not marked hidden.
|
||||
--shared Search for photos in shared iCloud album
|
||||
(Photos 5 only).
|
||||
(Photos 5+ only).
|
||||
--not-shared Search for photos not in shared iCloud album
|
||||
(Photos 5 only).
|
||||
(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.
|
||||
@@ -829,6 +853,17 @@ Options:
|
||||
units. For example, the following are all
|
||||
valid and equivalent sizes: '1048576'
|
||||
'1.048576MB', '1 MiB'.
|
||||
--missing Search for photos missing from disk.
|
||||
--not-missing Search for photos present on disk (e.g. not
|
||||
missing).
|
||||
--cloudasset Search for photos that are part of an iCloud
|
||||
library
|
||||
--not-cloudasset Search for photos that are not part of an
|
||||
iCloud library
|
||||
--incloud Search for photos that are in iCloud (have
|
||||
been synched)
|
||||
--not-incloud Search for photos that are not in iCloud (have
|
||||
not been synched)
|
||||
--regex REGEX TEMPLATE Search for photos where TEMPLATE matches
|
||||
regular expression REGEX. For example, to find
|
||||
photos in an album that begins with 'Beach': '
|
||||
@@ -876,12 +911,10 @@ 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.
|
||||
--deleted Include photos from the 'Recently Deleted'
|
||||
folder.
|
||||
--update Only export new or updated files. See also
|
||||
--force-update and notes below on export and
|
||||
--update.
|
||||
@@ -891,6 +924,17 @@ Options:
|
||||
would not otherwise trigger an export. See
|
||||
also --update and notes below on export and
|
||||
--update.
|
||||
--update-errors Update files that were previously exported but
|
||||
produced errors during export. For example, if
|
||||
a file produced an error with --exiftool due
|
||||
to bad metadata, this option will re-export
|
||||
the file and attempt to write the metadata
|
||||
again when used with --exiftool and --update.
|
||||
Without --update-errors, photos that were
|
||||
successfully exported but generated an error
|
||||
or warning during export will not be re-
|
||||
attempted if metadata has not changed. Must be
|
||||
used with --update.
|
||||
--ignore-signature When used with '--update', ignores file
|
||||
signature when updating files. This is useful
|
||||
if you have processed or edited exported
|
||||
@@ -1355,10 +1399,10 @@ Options:
|
||||
will be printed after the photo is exported or
|
||||
skipped. May be repeated to print multiple
|
||||
template strings.
|
||||
--theme THEME Specify the color theme to use for --verbose
|
||||
output. Valid themes are 'dark', 'light',
|
||||
'mono', and 'plain'. Defaults to 'dark' or
|
||||
'light' depending on system dark mode setting.
|
||||
--theme THEME Specify the color theme to use for output.
|
||||
Valid themes are 'dark', 'light', 'mono', and
|
||||
'plain'. Defaults to 'dark' or 'light'
|
||||
depending on system dark mode setting.
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Export
|
||||
@@ -1950,6 +1994,41 @@ Substitution Description
|
||||
as a 5-digit integer and pad with zeros, use
|
||||
'{id:05d}' which results in 00001, 00002,
|
||||
00003...etc.
|
||||
{counter} A sequential counter, starting at 0, that
|
||||
increments each time it is evaluated.To
|
||||
start counting at a value other than 0,
|
||||
append append '(starting_value)' to the
|
||||
field name.For example, to start counting at
|
||||
1 instead of 0: '{counter(1)}'.May be
|
||||
formatted using a python string format
|
||||
code.For example, to format as a 5-digit
|
||||
integer and pad with zeros, use
|
||||
'{counter:05d(1)}'which results in 00001,
|
||||
00002, 00003...etc.You may also specify a
|
||||
stop value which causes the counter to reset
|
||||
to the starting valuewhen the stop value is
|
||||
reached and a step size which causes the
|
||||
counter to increment bythe specified value
|
||||
instead of 1. Use the format
|
||||
'{counter(start,stop,step)}' where
|
||||
start,stop, and step are integers. For
|
||||
example, to count from 1 to 10 by 2, use
|
||||
'{counter(1,11,2)}'.Note that the counter
|
||||
stops counting when the stop value is
|
||||
reached and does not return thestop value.
|
||||
Start, stop, and step are optional and may
|
||||
be omitted. For example, to countfrom 0 by
|
||||
2s, use '{counter(,,2)}'.You may create an
|
||||
arbitrary number of counters by appending a
|
||||
unique name to the field namepreceded by a
|
||||
period: '{counter.a}', '{counter.b}', etc.
|
||||
Each counter will have its own stateand will
|
||||
start at 0 and increment by 1 unless
|
||||
otherwise specified. Note: {counter} is not
|
||||
suitable for use with 'export' and '--
|
||||
update' as the counter associated with a
|
||||
photo may change between export sessions.
|
||||
See also {id}.
|
||||
{album_seq} An integer, starting at 0, indicating the
|
||||
photo's index (sequence) in the containing
|
||||
album. Only valid when used in a '--
|
||||
@@ -2010,7 +2089,7 @@ Substitution Description
|
||||
{cr} A carriage return: '\r'
|
||||
{crlf} A carriage return + line feed: '\r\n'
|
||||
{tab} :A tab: '\t'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.54.4'
|
||||
{osxphotos_version} The osxphotos version, e.g. '0.57.3'
|
||||
{osxphotos_cmd_line} The full command line used to run osxphotos
|
||||
|
||||
The following substitutions may result in multiple values. Thus if specified
|
||||
@@ -2477,6 +2556,7 @@ The following template field substitutions are availabe for use the templating s
|
||||
|{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'|
|
||||
|{shortuuid}|A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'|
|
||||
|{id}|A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc. |
|
||||
|{counter}|A sequential counter, starting at 0, that increments each time it is evaluated.To start counting at a value other than 0, append append '(starting_value)' to the field name.For example, to start counting at 1 instead of 0: '{counter(1)}'.May be formatted using a python string format code.For example, to format as a 5-digit integer and pad with zeros, use '{counter:05d(1)}'which results in 00001, 00002, 00003...etc.You may also specify a stop value which causes the counter to reset to the starting valuewhen the stop value is reached and a step size which causes the counter to increment bythe specified value instead of 1. Use the format '{counter(start,stop,step)}' where start,stop, and step are integers. For example, to count from 1 to 10 by 2, use '{counter(1,11,2)}'.Note that the counter stops counting when the stop value is reached and does not return thestop value. Start, stop, and step are optional and may be omitted. For example, to countfrom 0 by 2s, use '{counter(,,2)}'.You may create an arbitrary number of counters by appending a unique name to the field namepreceded by a period: '{counter.a}', '{counter.b}', etc. Each counter will have its own stateand will start at 0 and increment by 1 unless otherwise specified. Note: {counter} is not suitable for use with 'export' and '--update' as the counter associated with a photo may change between export sessions. See also {id}.|
|
||||
|{album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album_seq}_{original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.|
|
||||
|{folder_album_seq}|An integer, starting at 0, indicating the photo's index (sequence) in the containing album and folder path. Only valid when used in a '--filename' template and only when '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{folder_album_seq}_{original_name}"'. To start counting at a value other than 0, append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{folder_album_seq(1)}' May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{folder_album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{folder_album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also '{album_seq}'. |
|
||||
|{comma}|A comma: ','|
|
||||
@@ -2494,7 +2574,7 @@ The following template field substitutions are availabe for use the templating s
|
||||
|{cr}|A carriage return: '\r'|
|
||||
|{crlf}|A carriage return + line feed: '\r\n'|
|
||||
|{tab}|:A tab: '\t'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.54.4'|
|
||||
|{osxphotos_version}|The osxphotos version, e.g. '0.57.3'|
|
||||
|{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|
|
||||
@@ -2546,58 +2626,73 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/britiscurious"><img src="https://avatars1.githubusercontent.com/u/25646439?v=4?s=75" width="75px;" alt="britiscurious"/><br /><sub><b>britiscurious</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mwort"><img src="https://avatars3.githubusercontent.com/u/8170417?v=4?s=75" width="75px;" alt="Michel Wortmann"/><br /><sub><b>Michel Wortmann</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mwort" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/PabloKohan"><img src="https://avatars3.githubusercontent.com/u/8790976?v=4?s=75" width="75px;" alt="Pablo 'merKur' Kohan"/><br /><sub><b>Pablo 'merKur' Kohan</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=PabloKohan" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hshore29"><img src="https://avatars2.githubusercontent.com/u/7023497?v=4?s=75" width="75px;" alt="hshore29"/><br /><sub><b>hshore29</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hshore29" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://3e.org/"><img src="https://avatars0.githubusercontent.com/u/41439?v=4?s=75" width="75px;" alt="Daniel M. Drucker"/><br /><sub><b>Daniel M. Drucker</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dmd" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Admd" title="Bug reports">🐛</a> <a href="#userTesting-dmd" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/jystervinou"><img src="https://avatars3.githubusercontent.com/u/132356?v=4?s=75" width="75px;" alt="Jean-Yves Stervinou"/><br /><sub><b>Jean-Yves Stervinou</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jystervinou" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://dethi.me/"><img src="https://avatars2.githubusercontent.com/u/1011520?v=4?s=75" width="75px;" alt="Thibault Deutsch"/><br /><sub><b>Thibault Deutsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dethi" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/britiscurious"><img src="https://avatars1.githubusercontent.com/u/25646439?v=4?s=75" width="75px;" alt="britiscurious"/><br /><sub><b>britiscurious</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=britiscurious" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mwort"><img src="https://avatars3.githubusercontent.com/u/8170417?v=4?s=75" width="75px;" alt="Michel Wortmann"/><br /><sub><b>Michel Wortmann</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mwort" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PabloKohan"><img src="https://avatars3.githubusercontent.com/u/8790976?v=4?s=75" width="75px;" alt="Pablo 'merKur' Kohan"/><br /><sub><b>Pablo 'merKur' Kohan</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=PabloKohan" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hshore29"><img src="https://avatars2.githubusercontent.com/u/7023497?v=4?s=75" width="75px;" alt="hshore29"/><br /><sub><b>hshore29</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hshore29" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://3e.org/"><img src="https://avatars0.githubusercontent.com/u/41439?v=4?s=75" width="75px;" alt="Daniel M. Drucker"/><br /><sub><b>Daniel M. Drucker</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dmd" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Admd" title="Bug reports">🐛</a> <a href="#userTesting-dmd" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jystervinou"><img src="https://avatars3.githubusercontent.com/u/132356?v=4?s=75" width="75px;" alt="Jean-Yves Stervinou"/><br /><sub><b>Jean-Yves Stervinou</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jystervinou" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://dethi.me/"><img src="https://avatars2.githubusercontent.com/u/1011520?v=4?s=75" width="75px;" alt="Thibault Deutsch"/><br /><sub><b>Thibault Deutsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dethi" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=75" width="75px;" alt="grundsch"/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=75" width="75px;" alt="Ag Primatic"/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=75" width="75px;" alt="Horst Höck"/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=75" width="75px;" alt="Jonathan Strine"/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=75" width="75px;" alt="finestream"/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=75" width="75px;" alt="Aravindo Wingeier"/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://kradalby.no"><img src="https://avatars1.githubusercontent.com/u/98431?v=4?s=75" width="75px;" alt="Kristoffer Dalby"/><br /><sub><b>Kristoffer Dalby</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=kradalby" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/grundsch"><img src="https://avatars0.githubusercontent.com/u/3874928?v=4?s=75" width="75px;" alt="grundsch"/><br /><sub><b>grundsch</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=grundsch" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/agprimatic"><img src="https://avatars1.githubusercontent.com/u/4685054?v=4?s=75" width="75px;" alt="Ag Primatic"/><br /><sub><b>Ag Primatic</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=agprimatic" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhoeck"><img src="https://avatars1.githubusercontent.com/u/6313998?v=4?s=75" width="75px;" alt="Horst Höck"/><br /><sub><b>Horst Höck</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hhoeck" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jstrine"><img src="https://avatars1.githubusercontent.com/u/33943447?v=4?s=75" width="75px;" alt="Jonathan Strine"/><br /><sub><b>Jonathan Strine</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=jstrine" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/finestream"><img src="https://avatars1.githubusercontent.com/u/16638513?v=4?s=75" width="75px;" alt="finestream"/><br /><sub><b>finestream</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=finestream" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/synox"><img src="https://avatars2.githubusercontent.com/u/2250964?v=4?s=75" width="75px;" alt="Aravindo Wingeier"/><br /><sub><b>Aravindo Wingeier</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=synox" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://kradalby.no"><img src="https://avatars1.githubusercontent.com/u/98431?v=4?s=75" width="75px;" alt="Kristoffer Dalby"/><br /><sub><b>Kristoffer Dalby</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=kradalby" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt="Rott-Apple"/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
<td align="center"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt="narensankar0529"/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt="Martin"/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||
<td align="center"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt="davidjroos "/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt="Neil Pankey"/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt="Aaron van Geffen"/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt="ubrandes "/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Rott-Apple"><img src="https://avatars1.githubusercontent.com/u/67875570?v=4?s=75" width="75px;" alt="Rott-Apple"/><br /><sub><b>Rott-Apple</b></sub></a><br /><a href="#research-Rott-Apple" title="Research">🔬</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/narensankar0529"><img src="https://avatars3.githubusercontent.com/u/74054766?v=4?s=75" width="75px;" alt="narensankar0529"/><br /><sub><b>narensankar0529</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anarensankar0529" title="Bug reports">🐛</a> <a href="#userTesting-narensankar0529" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/martinhrpi"><img src="https://avatars2.githubusercontent.com/u/19407684?v=4?s=75" width="75px;" alt="Martin"/><br /><sub><b>Martin</b></sub></a><br /><a href="#research-martinhrpi" title="Research">🔬</a> <a href="#userTesting-martinhrpi" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/davidjroos"><img src="https://avatars.githubusercontent.com/u/15630844?v=4?s=75" width="75px;" alt="davidjroos "/><br /><sub><b>davidjroos </b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=davidjroos" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://neilpa.me"><img src="https://avatars.githubusercontent.com/u/42419?v=4?s=75" width="75px;" alt="Neil Pankey"/><br /><sub><b>Neil Pankey</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=neilpa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://aaronweb.net/"><img src="https://avatars.githubusercontent.com/u/604665?v=4?s=75" width="75px;" alt="Aaron van Geffen"/><br /><sub><b>Aaron van Geffen</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=AaronVanGeffen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ubrandes"><img src="https://avatars.githubusercontent.com/u/59647284?v=4?s=75" width="75px;" alt="ubrandes "/><br /><sub><b>ubrandes </b></sub></a><br /><a href="#ideas-ubrandes" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://blog.dewost.com/"><img src="https://avatars.githubusercontent.com/u/17090228?v=4?s=75" width="75px;" alt="Philippe Dewost"/><br /><sub><b>Philippe Dewost</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=pdewost" title="Documentation">📖</a> <a href="#example-pdewost" title="Examples">💡</a> <a href="#ideas-pdewost" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt="kaduskj"/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt="mkirkland4874"/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||
<td align="center"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt="Joseph Commisso"/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||
<td align="center"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt="David Singer"/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt="oPromessa"/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a> <a href="#ideas-oPromessa" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="http://spencerchang.me"><img src="https://avatars.githubusercontent.com/u/14796580?v=4?s=75" width="75px;" alt="Spencer Chang"/><br /><sub><b>Spencer Chang</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aspencerc99" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://blog.dewost.com/"><img src="https://avatars.githubusercontent.com/u/17090228?v=4?s=75" width="75px;" alt="Philippe Dewost"/><br /><sub><b>Philippe Dewost</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=pdewost" title="Documentation">📖</a> <a href="#example-pdewost" title="Examples">💡</a> <a href="#ideas-pdewost" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kaduskj"><img src="https://avatars.githubusercontent.com/u/983067?v=4?s=75" width="75px;" alt="kaduskj"/><br /><sub><b>kaduskj</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Akaduskj" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mkirkland4874"><img src="https://avatars.githubusercontent.com/u/36466711?v=4?s=75" width="75px;" alt="mkirkland4874"/><br /><sub><b>mkirkland4874</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Amkirkland4874" title="Bug reports">🐛</a> <a href="#example-mkirkland4874" title="Examples">💡</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jcommisso07"><img src="https://avatars.githubusercontent.com/u/3111054?v=4?s=75" width="75px;" alt="Joseph Commisso"/><br /><sub><b>Joseph Commisso</b></sub></a><br /><a href="#data-jcommisso07" title="Data">🔣</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dssinger"><img src="https://avatars.githubusercontent.com/u/1817903?v=4?s=75" width="75px;" alt="David Singer"/><br /><sub><b>David Singer</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adssinger" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/oPromessa"><img src="https://avatars.githubusercontent.com/u/21261491?v=4?s=75" width="75px;" alt="oPromessa"/><br /><sub><b>oPromessa</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3AoPromessa" title="Bug reports">🐛</a> <a href="#ideas-oPromessa" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Tests">⚠️</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=oPromessa" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://spencerchang.me"><img src="https://avatars.githubusercontent.com/u/14796580?v=4?s=75" width="75px;" alt="Spencer Chang"/><br /><sub><b>Spencer Chang</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aspencerc99" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://www.cs.purdue.edu/homes/dgleich"><img src="https://avatars.githubusercontent.com/u/33995?v=4?s=75" width="75px;" alt="David Gleich"/><br /><sub><b>David Gleich</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dgleich" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt="Alan de Freitas"/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt="Andrew Louis"/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt="neebah"/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt="Ahti Liin"/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="https://github.com/xwu64"><img src="https://avatars.githubusercontent.com/u/10580396?v=4?s=75" width="75px;" alt="Xiaoliang Wu"/><br /><sub><b>Xiaoliang Wu</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=xwu64" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/nullpointerninja"><img src="https://avatars.githubusercontent.com/u/62975432?v=4?s=75" width="75px;" alt="nullpointerninja"/><br /><sub><b>nullpointerninja</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anullpointerninja" title="Bug reports">🐛</a> <a href="#ideas-nullpointerninja" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://www.cs.purdue.edu/homes/dgleich"><img src="https://avatars.githubusercontent.com/u/33995?v=4?s=75" width="75px;" alt="David Gleich"/><br /><sub><b>David Gleich</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dgleich" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://alandefreitas.github.io/alandefreitas/"><img src="https://avatars.githubusercontent.com/u/5369819?v=4?s=75" width="75px;" alt="Alan de Freitas"/><br /><sub><b>Alan de Freitas</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aalandefreitas" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://hyfen.net"><img src="https://avatars.githubusercontent.com/u/6291?v=4?s=75" width="75px;" alt="Andrew Louis"/><br /><sub><b>Andrew Louis</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Documentation">📖</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=hyfen" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/neebah"><img src="https://avatars.githubusercontent.com/u/71442026?v=4?s=75" width="75px;" alt="neebah"/><br /><sub><b>neebah</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aneebah" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ahti123"><img src="https://avatars.githubusercontent.com/u/22232632?v=4?s=75" width="75px;" alt="Ahti Liin"/><br /><sub><b>Ahti Liin</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=ahti123" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aahti123" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/xwu64"><img src="https://avatars.githubusercontent.com/u/10580396?v=4?s=75" width="75px;" alt="Xiaoliang Wu"/><br /><sub><b>Xiaoliang Wu</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=xwu64" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/nullpointerninja"><img src="https://avatars.githubusercontent.com/u/62975432?v=4?s=75" width="75px;" alt="nullpointerninja"/><br /><sub><b>nullpointerninja</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Anullpointerninja" title="Bug reports">🐛</a> <a href="#ideas-nullpointerninja" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/infused-kim"><img src="https://avatars.githubusercontent.com/u/7404004?v=4?s=75" width="75px;" alt="Kim"/><br /><sub><b>Kim</b></sub></a><br /><a href="#ideas-infused-kim" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://github.com/Se7enair"><img src="https://avatars.githubusercontent.com/u/1680106?v=4?s=75" width="75px;" alt="Christoph"/><br /><sub><b>Christoph</b></sub></a><br /><a href="#ideas-Se7enair" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="http://www.franzone.com"><img src="https://avatars.githubusercontent.com/u/900684?v=4?s=75" width="75px;" alt="franzone"/><br /><sub><b>franzone</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afranzone" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://jmuccigr.github.io/"><img src="https://avatars.githubusercontent.com/u/615115?v=4?s=75" width="75px;" alt="John Muccigrosso"/><br /><sub><b>John Muccigrosso</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajmuccigr" title="Bug reports">🐛</a> <a href="#ideas-jmuccigr" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center"><a href="https://nomadgate.com"><img src="https://avatars.githubusercontent.com/u/1646041?v=4?s=75" width="75px;" alt="Thomas K. Running"/><br /><sub><b>Thomas K. Running</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=tkrunning" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Atkrunning" title="Bug reports">🐛</a></td>
|
||||
<td align="center"><a href="http://dalisoft.uz"><img src="https://avatars.githubusercontent.com/u/3511344?v=4?s=75" width="75px;" alt="Davlatjon Shavkatov"/><br /><sub><b>Davlatjon Shavkatov</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/zephyr325"><img src="https://avatars.githubusercontent.com/u/5245609?v=4?s=75" width="75px;" alt="zephyr325"/><br /><sub><b>zephyr325</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Azephyr325" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/infused-kim"><img src="https://avatars.githubusercontent.com/u/7404004?v=4?s=75" width="75px;" alt="Kim"/><br /><sub><b>Kim</b></sub></a><br /><a href="#ideas-infused-kim" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Se7enair"><img src="https://avatars.githubusercontent.com/u/1680106?v=4?s=75" width="75px;" alt="Christoph"/><br /><sub><b>Christoph</b></sub></a><br /><a href="#ideas-Se7enair" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.franzone.com"><img src="https://avatars.githubusercontent.com/u/900684?v=4?s=75" width="75px;" alt="franzone"/><br /><sub><b>franzone</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afranzone" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://jmuccigr.github.io/"><img src="https://avatars.githubusercontent.com/u/615115?v=4?s=75" width="75px;" alt="John Muccigrosso"/><br /><sub><b>John Muccigrosso</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajmuccigr" title="Bug reports">🐛</a> <a href="#ideas-jmuccigr" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://nomadgate.com"><img src="https://avatars.githubusercontent.com/u/1646041?v=4?s=75" width="75px;" alt="Thomas K. Running"/><br /><sub><b>Thomas K. Running</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=tkrunning" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Atkrunning" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://dalisoft.uz"><img src="https://avatars.githubusercontent.com/u/3511344?v=4?s=75" width="75px;" alt="Davlatjon Shavkatov"/><br /><sub><b>Davlatjon Shavkatov</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=dalisoft" title="Tests">⚠️</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/zephyr325"><img src="https://avatars.githubusercontent.com/u/5245609?v=4?s=75" width="75px;" alt="zephyr325"/><br /><sub><b>zephyr325</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Azephyr325" title="Bug reports">🐛</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/drodner"><img src="https://avatars.githubusercontent.com/u/10236892?v=4?s=75" width="75px;" alt="drodner"/><br /><sub><b>drodner</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Adrodner" title="Bug reports">🐛</a> <a href="#userTesting-drodner" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://fmckeogh.github.io"><img src="https://avatars.githubusercontent.com/u/8290136?v=4?s=75" width="75px;" alt="Ferdia McKeogh"/><br /><sub><b>Ferdia McKeogh</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=fmckeogh" title="Code">💻</a> <a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Afmckeogh" title="Bug reports">🐛</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://wellsaidlabs.com"><img src="https://avatars.githubusercontent.com/u/7424737?v=4?s=75" width="75px;" alt="Michael Petrochuk"/><br /><sub><b>Michael Petrochuk</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3APetrochukM" title="Bug reports">🐛</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=PetrochukM" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://qkeddy.github.io/quin-eddy-development-portfolio/"><img src="https://avatars.githubusercontent.com/u/9737814?v=4?s=75" width="75px;" alt="Quin Eddy"/><br /><sub><b>Quin Eddy</b></sub></a><br /><a href="#ideas-qkeddy" title="Ideas, Planning, & Feedback">🤔</a> <a href="#data-qkeddy" title="Data">🔣</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/johnsturgeon"><img src="https://avatars.githubusercontent.com/u/9746310?v=4?s=75" width="75px;" alt="John Sturgeon"/><br /><sub><b>John Sturgeon</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Ajohnsturgeon" title="Bug reports">🐛</a> <a href="https://github.com/RhetTbull/osxphotos/commits?author=johnsturgeon" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/mave2k"><img src="https://avatars.githubusercontent.com/u/8629837?v=4?s=75" width="75px;" alt="mave2k"/><br /><sub><b>mave2k</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/commits?author=mave2k" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://danielbeadle.net"><img src="https://avatars.githubusercontent.com/u/6235378?v=4?s=75" width="75px;" alt="Daniel Beadle"/><br /><sub><b>Daniel Beadle</b></sub></a><br /><a href="#ideas-djbeadle" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://eecue.com/"><img src="https://avatars.githubusercontent.com/u/532536?v=4?s=75" width="75px;" alt="Dave Bullock"/><br /><sub><b>Dave Bullock</b></sub></a><br /><a href="#ideas-eecue" title="Ideas, Planning, & Feedback">🤔</a> <a href="#userTesting-eecue" title="User Testing">📓</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pweaver"><img src="https://avatars.githubusercontent.com/u/611620?v=4?s=75" width="75px;" alt="Pweaver"/><br /><sub><b>Pweaver</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Apweaver" title="Bug reports">🐛</a> <a href="#ideas-pweaver" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/aa599"><img src="https://avatars.githubusercontent.com/u/37746269?v=4?s=75" width="75px;" alt="aa599"/><br /><sub><b>aa599</b></sub></a><br /><a href="https://github.com/RhetTbull/osxphotos/issues?q=author%3Aaa599" title="Bug reports">🐛</a> <a href="#ideas-aa599" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/swduncan"><img src="https://avatars.githubusercontent.com/u/2053195?v=4?s=75" width="75px;" alt="Steve Duncan"/><br /><sub><b>Steve Duncan</b></sub></a><br /><a href="#ideas-swduncan" title="Ideas, Planning, & Feedback">🤔</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -21,7 +21,7 @@ Only works on macOS (aka Mac OS X). Tested on macOS Sierra (10.12.6) through mac
|
||||
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.8``.
|
||||
Requires python >= ``3.9``.
|
||||
|
||||
Installation
|
||||
------------
|
||||
|
||||
10
build.sh
10
build.sh
@@ -12,6 +12,7 @@ cog -d -o osxphotos/phototemplate.md osxphotos/phototemplate.cog.md
|
||||
|
||||
echo "Updating README.md"
|
||||
python3 utils/update_readme.py
|
||||
cog -r README.md
|
||||
|
||||
echo "Updating API_README.md"
|
||||
cog -r API_README.md
|
||||
@@ -48,6 +49,9 @@ echo "Building CLI executable"
|
||||
|
||||
# zip up CLI executable
|
||||
echo "Zipping CLI executable"
|
||||
OSXPHOTOS_VERSION=$(python3 -c "import osxphotos; print(osxphotos.__version__)")
|
||||
zip dist/osxphotos_MacOS_exe_darwin_x64_v$OSXPHOTOS_VERSION.zip dist/osxphotos
|
||||
rm dist/osxphotos
|
||||
OSXPHOTOSVERSION=$(python3 -c "import osxphotos; print(osxphotos.__version__)")
|
||||
ARCHSTR=$(uname -m)
|
||||
ZIPNAME=osxphotos_MacOS_exe_darwin_${ARCHSTR}_v${OSXPHOTOSVERSION}.zip
|
||||
echo "Zipping CLI executable to $ZIPNAME"
|
||||
cd dist && zip $ZIPNAME osxphotos && cd ..
|
||||
rm dist/osxphotos
|
||||
|
||||
@@ -14,4 +14,5 @@ sphinx_rtd_theme
|
||||
sphinx-copybutton
|
||||
sphinxcontrib-programoutput
|
||||
twine
|
||||
wheel
|
||||
wheel
|
||||
setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability
|
||||
@@ -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: 45f2dda1d3ddb8cbb89b1f561aef11df
|
||||
config: 2de8a81a5b8629229e1e320af3fcdcb2
|
||||
tags: 645f666f9bcd5a90fca523b33c5a78b7
|
||||
|
||||
3073
docs/API_README.html
Normal file
3073
docs/API_README.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../genindex.html" /><link rel="search" title="Search" href="../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>Overview: module code - osxphotos 0.54.4 documentation</title>
|
||||
<title>Overview: module code - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="../index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -160,7 +160,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos._constants - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos._constants - osxphotos 0.57.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.57.1 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.1 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -195,9 +195,9 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos._constants</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">"""</span>
|
||||
<span class="sd">Constants used by osxphotos </span>
|
||||
<span class="sd">"""</span>
|
||||
<span></span><span class="sd">""" Constants used by osxphotos """</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">os.path</span>
|
||||
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
|
||||
@@ -318,6 +318,8 @@
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"4"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"5"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"12"</span><span class="p">,</span> <span class="s2">"6"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"13"</span><span class="p">,</span> <span class="s2">"0"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"13"</span><span class="p">,</span> <span class="s2">"1"</span><span class="p">),</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="c1"># Photos 5 has persons who are empty string if unidentified face</span>
|
||||
@@ -330,6 +332,13 @@
|
||||
|
||||
<span class="c1"># Where are shared iCloud photos located?</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span> <span class="o">=</span> <span class="s2">"resources/cloudsharing/data"</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span> <span class="o">=</span> <span class="s2">"scopes/cloudsharing/data"</span>
|
||||
|
||||
<span class="c1"># Where are shared iCloud derivatives located?</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="s2">"resources/cloudsharing/resources/derivatives/masters"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span> <span class="o">=</span> <span class="s2">"scopes/cloudsharing/resources/derivatives/masters"</span>
|
||||
|
||||
<span class="c1"># What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database</span>
|
||||
<span class="n">_PHOTO_TYPE</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
@@ -368,6 +377,9 @@
|
||||
<span class="c1"># If anyone has a keyword matching this, then too bad...</span>
|
||||
<span class="n">_OSXPHOTOS_NONE_SENTINEL</span> <span class="o">=</span> <span class="s2">"OSXPhotosXYZZY42_Sentinel$"</span>
|
||||
|
||||
<span class="c1"># Lock file extension for reserving filenames when exporting</span>
|
||||
<span class="n">_OSXPHOTOS_LOCK_EXTENSION</span> <span class="o">=</span> <span class="s2">".osxphotos.lock"</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">SearchCategory</span><span class="p">:</span>
|
||||
<span class="sd">"""SearchInfo categories for Photos 5+; corresponds to categories in database/search/psi.sqlite:groups.category</span>
|
||||
@@ -403,11 +415,11 @@
|
||||
<span class="n">TITLE</span> <span class="o">=</span> <span class="mi">2017</span>
|
||||
<span class="n">DESCRIPTION</span> <span class="o">=</span> <span class="mi">2018</span>
|
||||
<span class="n">HOME</span> <span class="o">=</span> <span class="mi">2020</span>
|
||||
<span class="n">WORK</span> <span class="o">=</span> <span class="mi">2036</span>
|
||||
<span class="n">PERSON</span> <span class="o">=</span> <span class="mi">2021</span>
|
||||
<span class="n">ACTIVITY</span> <span class="o">=</span> <span class="mi">2027</span>
|
||||
<span class="n">HOLIDAY</span> <span class="o">=</span> <span class="mi">2029</span>
|
||||
<span class="n">SEASON</span> <span class="o">=</span> <span class="mi">2030</span>
|
||||
<span class="n">WORK</span> <span class="o">=</span> <span class="mi">2036</span>
|
||||
<span class="n">VENUE</span> <span class="o">=</span> <span class="mi">2038</span>
|
||||
<span class="n">VENUE_TYPE</span> <span class="o">=</span> <span class="mi">2039</span>
|
||||
<span class="n">PHOTO_TYPE_VIDEO</span> <span class="o">=</span> <span class="mi">2044</span>
|
||||
@@ -420,6 +432,7 @@
|
||||
<span class="n">PHOTO_TYPE_PORTRAIT</span> <span class="o">=</span> <span class="mi">2053</span>
|
||||
<span class="n">PHOTO_TYPE_SELFIES</span> <span class="o">=</span> <span class="mi">2054</span>
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span> <span class="o">=</span> <span class="mi">2055</span>
|
||||
<span class="n">PHOTO_TYPE_ANIMATED</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">MEDIA_TYPES</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="n">PHOTO_TYPE_VIDEO</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_SLOMO</span><span class="p">,</span>
|
||||
@@ -433,15 +446,45 @@
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">PHOTO_NAME</span> <span class="o">=</span> <span class="mi">2056</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">TEXT_FOUND</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
<span class="n">SOURCE</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 8+ only</span>
|
||||
|
||||
<span class="nd">@classmethod</span>
|
||||
<span class="k">def</span> <span class="nf">categories</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span> <span class="o">-></span> <span class="nb">dict</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">str</span><span class="p">]:</span>
|
||||
<span class="sd">"""Return categories as dict of value: name"""</span>
|
||||
<span class="c1"># a bit of a hack to basically reverse the enum</span>
|
||||
<span class="k">return</span> <span class="p">{</span>
|
||||
<span class="n">value</span><span class="p">:</span> <span class="n">name</span>
|
||||
<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="bp">cls</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">name</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="n">name</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"__"</span><span class="p">)</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
|
||||
<span class="ow">and</span> <span class="n">name</span><span class="o">.</span><span class="n">isupper</span><span class="p">()</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">dict</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">))</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">SearchCategory_Photos8</span><span class="p">(</span><span class="n">SearchCategory</span><span class="p">):</span>
|
||||
<span class="sd">"""Search categories for Photos 8"""</span>
|
||||
|
||||
|
||||
<span class="c1"># Many of the category values changed in Ventura / Photos 8</span>
|
||||
<span class="c1"># and some new categories were added</span>
|
||||
<span class="n">CITY</span> <span class="o">=</span> <span class="mi">5</span>
|
||||
<span class="n">LOCALITY_4</span> <span class="o">=</span> <span class="mi">4</span>
|
||||
<span class="n">SUB_LOCALITY_5</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">SUB_LOCALITY_6</span> <span class="o">=</span> <span class="mi">6</span>
|
||||
<span class="n">LOCALITY_8</span> <span class="o">=</span> <span class="mi">8</span>
|
||||
<span class="n">NAMED_AREA</span> <span class="o">=</span> <span class="mi">7</span>
|
||||
<span class="n">ALL_LOCALITY</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="n">LOCALITY_4</span><span class="p">,</span>
|
||||
<span class="n">SUB_LOCALITY_6</span><span class="p">,</span>
|
||||
<span class="n">LOCALITY_8</span><span class="p">,</span>
|
||||
<span class="n">NAMED_AREA</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">HOME</span> <span class="o">=</span> <span class="mi">1000</span>
|
||||
<span class="n">WORK</span> <span class="o">=</span> <span class="mi">1001</span>
|
||||
<span class="n">LABEL</span> <span class="o">=</span> <span class="mi">1500</span>
|
||||
<span class="n">MONTH</span> <span class="o">=</span> <span class="mi">1100</span>
|
||||
<span class="n">YEAR</span> <span class="o">=</span> <span class="mi">1101</span>
|
||||
@@ -450,12 +493,56 @@
|
||||
<span class="n">KEYWORDS</span> <span class="o">=</span> <span class="mi">1200</span>
|
||||
<span class="n">TITLE</span> <span class="o">=</span> <span class="mi">1201</span>
|
||||
<span class="n">DESCRIPTION</span> <span class="o">=</span> <span class="mi">1202</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="mi">1203</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">DETECTED_TEXT</span> <span class="o">=</span> <span class="mi">1203</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">TEXT_FOUND</span> <span class="o">=</span> <span class="mi">1205</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">PERSON</span> <span class="o">=</span> <span class="mi">1300</span>
|
||||
<span class="n">ACTIVITY</span> <span class="o">=</span> <span class="mi">1600</span>
|
||||
<span class="n">VENUE</span> <span class="o">=</span> <span class="mi">1700</span>
|
||||
<span class="n">VENUE_TYPE</span> <span class="o">=</span> <span class="mi">1701</span>
|
||||
<span class="n">PHOTO_TYPE_VIDEO</span> <span class="o">=</span> <span class="mi">1901</span>
|
||||
<span class="n">PHOTO_TYPE_SELFIES</span> <span class="o">=</span> <span class="mi">1915</span>
|
||||
<span class="n">PHOTO_TYPE_LIVE</span> <span class="o">=</span> <span class="mi">1906</span>
|
||||
<span class="n">PHOTO_TYPE_PORTRAIT</span> <span class="o">=</span> <span class="mi">1914</span>
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span> <span class="o">=</span> <span class="mi">2000</span>
|
||||
<span class="n">PHOTO_TYPE_PANORAMA</span> <span class="o">=</span> <span class="mi">1908</span>
|
||||
<span class="n">PHOTO_TYPE_TIMELAPSE</span> <span class="o">=</span> <span class="mi">1909</span>
|
||||
<span class="n">PHOTO_TYPE_SLOMO</span> <span class="o">=</span> <span class="mi">1905</span>
|
||||
<span class="n">PHOTO_TYPE_BURSTS</span> <span class="o">=</span> <span class="mi">1913</span>
|
||||
<span class="n">PHOTO_TYPE_SCREENSHOT</span> <span class="o">=</span> <span class="mi">1907</span>
|
||||
<span class="n">PHOTO_TYPE_ANIMATED</span> <span class="o">=</span> <span class="mi">1912</span>
|
||||
<span class="n">PHOTO_TYPE_RAW</span> <span class="o">=</span> <span class="mi">1902</span>
|
||||
<span class="n">MEDIA_TYPES</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="n">PHOTO_TYPE_VIDEO</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_SLOMO</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_LIVE</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_SCREENSHOT</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_PANORAMA</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_TIMELAPSE</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_BURSTS</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_PORTRAIT</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_SELFIES</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_FAVORITES</span><span class="p">,</span>
|
||||
<span class="n">PHOTO_TYPE_ANIMATED</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">PHOTO_NAME</span> <span class="o">=</span> <span class="mi">2100</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="mi">2300</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">CAMERA</span> <span class="o">=</span> <span class="mi">2300</span> <span class="c1"># new in Photos 8</span>
|
||||
<span class="n">SOURCE</span> <span class="o">=</span> <span class="mi">2200</span> <span class="c1"># new in Photos 8, shows the app/software source for the photo, e.g. Messages, Safari, etc.</span>
|
||||
|
||||
<span class="nd">@classmethod</span>
|
||||
<span class="k">def</span> <span class="nf">categories</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span> <span class="o">-></span> <span class="nb">dict</span><span class="p">[</span><span class="nb">int</span><span class="p">,</span> <span class="nb">str</span><span class="p">]:</span>
|
||||
<span class="sd">"""Return categories as dict of value: name"""</span>
|
||||
<span class="c1"># need to get the categories from the base class and update with the new values</span>
|
||||
<span class="n">classdict</span> <span class="o">=</span> <span class="n">SearchCategory</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
<span class="n">classdict</span> <span class="o">|=</span> <span class="bp">cls</span><span class="o">.</span><span class="vm">__dict__</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="p">{</span>
|
||||
<span class="n">value</span><span class="p">:</span> <span class="n">name</span>
|
||||
<span class="k">for</span> <span class="n">name</span><span class="p">,</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">classdict</span><span class="o">.</span><span class="n">items</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">name</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="n">name</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"__"</span><span class="p">)</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="n">callable</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
|
||||
<span class="ow">and</span> <span class="n">name</span><span class="o">.</span><span class="n">isupper</span><span class="p">()</span>
|
||||
<span class="ow">and</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="p">(</span><span class="nb">list</span><span class="p">,</span> <span class="nb">dict</span><span class="p">,</span> <span class="nb">tuple</span><span class="p">))</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">search_category_factory</span><span class="p">(</span><span class="n">version</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-></span> <span class="n">SearchCategory</span><span class="p">:</span>
|
||||
@@ -464,7 +551,7 @@
|
||||
|
||||
|
||||
<span class="c1"># Max filename length on MacOS</span>
|
||||
<span class="n">MAX_FILENAME_LEN</span> <span class="o">=</span> <span class="mi">255</span>
|
||||
<span class="n">MAX_FILENAME_LEN</span> <span class="o">=</span> <span class="mi">255</span> <span class="o">-</span> <span class="nb">len</span><span class="p">(</span><span class="n">_OSXPHOTOS_LOCK_EXTENSION</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># Max directory name length on MacOS</span>
|
||||
<span class="n">MAX_DIRNAME_LEN</span> <span class="o">=</span> <span class="mi">255</span>
|
||||
@@ -569,6 +656,10 @@
|
||||
<span class="s2">"time"</span><span class="p">,</span>
|
||||
<span class="s2">"tottime"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="n">UUID_PATTERN</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="sa">r</span><span class="s2">"[0-9a-fA-F]</span><span class="si">{8}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{4}</span><span class="s2">-[0-9a-fA-F]</span><span class="si">{12}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
</pre></div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
@@ -1,38 +1,204 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.debug - osxphotos 0.56.7 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.debug — osxphotos 0.47.9 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css" />
|
||||
<script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f8f8f8;
|
||||
--color-code-foreground: black;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M13 12h5" />
|
||||
<path d="M13 15h4" />
|
||||
<path d="M13 18h1" />
|
||||
<path d="M13 9h4" />
|
||||
<path d="M13 6h1" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
||||
<label class="overlay sidebar-overlay" for="__navigation">
|
||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
||||
</label>
|
||||
<label class="overlay toc-overlay" for="__toc">
|
||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.7 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.7 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.debug</h1><div class="highlight"><pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.debug</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">"""Utilities for debugging"""</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
<span class="kn">import</span> <span class="nn">sys</span>
|
||||
<span class="kn">import</span> <span class="nn">time</span>
|
||||
@@ -42,21 +208,33 @@
|
||||
<span class="kn">import</span> <span class="nn">wrapt</span>
|
||||
<span class="kn">from</span> <span class="nn">rich</span> <span class="kn">import</span> <span class="nb">print</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"debug_breakpoint"</span><span class="p">,</span>
|
||||
<span class="s2">"debug_watch"</span><span class="p">,</span>
|
||||
<span class="s2">"get_debug_flags"</span><span class="p">,</span>
|
||||
<span class="s2">"get_debug_options"</span><span class="p">,</span>
|
||||
<span class="s2">"is_debug"</span><span class="p">,</span>
|
||||
<span class="s2">"set_debug"</span><span class="p">,</span>
|
||||
<span class="s2">"wrap_function"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
|
||||
<span class="c1"># global variable to control debug output</span>
|
||||
<span class="c1"># set via --debug</span>
|
||||
<span class="n">DEBUG</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="n">__osxphotos_debug</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="set_debug"><a class="viewcode-back" href="../../reference.html#osxphotos.set_debug">[docs]</a><span class="k">def</span> <span class="nf">set_debug</span><span class="p">(</span><span class="n">debug</span><span class="p">:</span> <span class="nb">bool</span><span class="p">):</span>
|
||||
<span class="sd">"""set debug flag"""</span>
|
||||
<span class="k">global</span> <span class="n">DEBUG</span>
|
||||
<span class="n">DEBUG</span> <span class="o">=</span> <span class="n">debug</span>
|
||||
<span class="k">global</span> <span class="n">__osxphotos_debug</span>
|
||||
<span class="n">__osxphotos_debug</span> <span class="o">=</span> <span class="n">debug</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">disable</span><span class="p">(</span><span class="n">logging</span><span class="o">.</span><span class="n">NOTSET</span> <span class="k">if</span> <span class="n">debug</span> <span class="k">else</span> <span class="n">logging</span><span class="o">.</span><span class="n">DEBUG</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="is_debug"><a class="viewcode-back" href="../../reference.html#osxphotos.is_debug">[docs]</a><span class="k">def</span> <span class="nf">is_debug</span><span class="p">():</span>
|
||||
<span class="sd">"""return debug flag"""</span>
|
||||
<span class="k">return</span> <span class="n">DEBUG</span></div>
|
||||
<span class="k">global</span> <span class="n">__osxphotos_debug</span>
|
||||
<span class="k">return</span> <span class="n">__osxphotos_debug</span></div>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">debug_watch</span><span class="p">(</span><span class="n">wrapped</span><span class="p">,</span> <span class="n">instance</span><span class="p">,</span> <span class="n">args</span><span class="p">,</span> <span class="n">kwargs</span><span class="p">):</span>
|
||||
@@ -82,7 +260,7 @@
|
||||
|
||||
<span class="k">def</span> <span class="nf">wrap_function</span><span class="p">(</span><span class="n">function_path</span><span class="p">,</span> <span class="n">wrapper</span><span class="p">):</span>
|
||||
<span class="sd">"""Wrap a function with wrapper function"""</span>
|
||||
<span class="n">module</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="n">function_path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"."</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
<span class="n">module</span><span class="p">,</span> <span class="n">name</span> <span class="o">=</span> <span class="n">function_path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">"::"</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">wrapt</span><span class="o">.</span><span class="n">wrap_function_wrapper</span><span class="p">(</span><span class="n">module</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">wrapper</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
@@ -135,72 +313,47 @@
|
||||
<span class="n">args</span><span class="p">[</span><span class="n">arg_name</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="k">return</span> <span class="n">args</span>
|
||||
</pre></div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="../../index.html">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">osxphotos package</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="../../index.html">Documentation overview</a><ul>
|
||||
<li><a href="../index.html">Module code</a><ul>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2021, Rhet Turnbull
|
||||
</div>
|
||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
||||
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
<div class="icons">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
<aside class="toc-drawer no-toc">
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
|
||||
<title>osxphotos.exiftool - osxphotos 0.50.13 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.exiftool - osxphotos 0.56.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.50.13 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.4 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.50.13 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.4 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -179,7 +179,8 @@
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container"><div class="theme-toggle-container theme-toggle-content">
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
@@ -194,13 +195,13 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.exiftool</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" Yet another simple exiftool wrapper </span>
|
||||
<span></span><span class="sd">""" Yet another simple exiftool wrapper </span>
|
||||
<span class="sd"> I rolled my own for following reasons: </span>
|
||||
<span class="sd"> 1. I wanted something under MIT license (best alternative was licensed under GPL/BSD)</span>
|
||||
<span class="sd"> 2. I wanted singleton behavior so only a single exiftool process was ever running</span>
|
||||
<span class="sd"> 3. When used as a context manager, I wanted the operations to batch until exiting the context (improved performance)</span>
|
||||
<span class="sd"> If these aren't important to you, I highly recommend you use Sven Marnach's excellent </span>
|
||||
<span class="sd"> pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """</span>
|
||||
<span class="sd"> If these aren't important to you, I highly recommend you use Sven Marnach's excellent </span>
|
||||
<span class="sd"> pyexiftool: https://github.com/smarnach/pyexiftool which provides more functionality """</span>
|
||||
|
||||
|
||||
<span class="kn">import</span> <span class="nn">atexit</span>
|
||||
@@ -217,153 +218,154 @@
|
||||
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">lru_cache</span> <span class="c1"># pylint: disable=syntax-error</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"escape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"exiftool_can_write"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifTool"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifToolCaching"</span><span class="p">,</span>
|
||||
<span class="s2">"get_exiftool_path"</span><span class="p">,</span>
|
||||
<span class="s2">"terminate_exiftool"</span><span class="p">,</span>
|
||||
<span class="s2">"unescape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"escape_str"</span><span class="p">,</span>
|
||||
<span class="s2">"exiftool_can_write"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifTool"</span><span class="p">,</span>
|
||||
<span class="s2">"ExifToolCaching"</span><span class="p">,</span>
|
||||
<span class="s2">"get_exiftool_path"</span><span class="p">,</span>
|
||||
<span class="s2">"terminate_exiftool"</span><span class="p">,</span>
|
||||
<span class="s2">"unescape_str"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
|
||||
<span class="c1"># exiftool -stay_open commands outputs this EOF marker after command is run</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="o">=</span> <span class="s2">"</span><span class="si">{ready}</span><span class="s2">"</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="o">=</span> <span class="s2">"</span><span class="si">{ready}</span><span class="s2">"</span>
|
||||
<span class="n">EXIFTOOL_STAYOPEN_EOF_LEN</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">EXIFTOOL_STAYOPEN_EOF</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># list of exiftool processes to cleanup when exiting or when terminate is called</span>
|
||||
<span class="n">EXIFTOOL_PROCESSES</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
|
||||
<span class="c1"># exiftool supported file types, created by utils/exiftool_supported_types.py</span>
|
||||
<span class="n">EXIFTOOL_FILETYPES_JSON</span> <span class="o">=</span> <span class="s2">"exiftool_filetypes.json"</span>
|
||||
<span class="k">with</span> <span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="n">EXIFTOOL_FILETYPES_JSON</span><span class="p">)</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="n">EXIFTOOL_FILETYPES_JSON</span> <span class="o">=</span> <span class="s2">"exiftool_filetypes.json"</span>
|
||||
<span class="k">with</span> <span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="vm">__file__</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span> <span class="o">/</span> <span class="n">EXIFTOOL_FILETYPES_JSON</span><span class="p">)</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s2">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">exiftool_can_write</span><span class="p">(</span><span class="n">suffix</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">bool</span><span class="p">:</span>
|
||||
<span class="sd">"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""</span>
|
||||
<span class="sd">"""Return True if exiftool supports writing to a file with the given suffix, otherwise False"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">suffix</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
<span class="n">suffix</span> <span class="o">=</span> <span class="n">suffix</span><span class="o">.</span><span class="n">lower</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"."</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"."</span><span class="p">:</span>
|
||||
<span class="n">suffix</span> <span class="o">=</span> <span class="n">suffix</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="n">suffix</span> <span class="ow">in</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span>
|
||||
<span class="ow">and</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span><span class="p">[</span><span class="n">suffix</span><span class="p">][</span><span class="s2">"write"</span><span class="p">]</span>
|
||||
<span class="ow">and</span> <span class="n">EXIFTOOL_SUPPORTED_FILETYPES</span><span class="p">[</span><span class="n">suffix</span><span class="p">][</span><span class="s2">"write"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">escape_str</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
|
||||
<span class="sd">"""escape string for use with exiftool -E"""</span>
|
||||
<span class="sd">"""escape string for use with exiftool -E"""</span>
|
||||
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">html</span><span class="o">.</span><span class="n">escape</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xa;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\t</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#x9;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xd;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xa;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\t</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#x9;"</span><span class="p">)</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"</span><span class="se">\r</span><span class="s2">"</span><span class="p">,</span> <span class="s2">"&#xd;"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">unescape_str</span><span class="p">(</span><span class="n">s</span><span class="p">):</span>
|
||||
<span class="sd">"""unescape an HTML string returned by exiftool -E"""</span>
|
||||
<span class="sd">"""unescape an HTML string returned by exiftool -E"""</span>
|
||||
<span class="k">if</span> <span class="nb">type</span><span class="p">(</span><span class="n">s</span><span class="p">)</span> <span class="o">!=</span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">s</span>
|
||||
<span class="c1"># avoid " in values which result in json.loads() throwing an exception, #636</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"&quot;"</span><span class="p">,</span> <span class="s1">'</span><span class="se">\\</span><span class="s1">"'</span><span class="p">)</span>
|
||||
<span class="c1"># avoid " in values which result in json.loads() throwing an exception, #636</span>
|
||||
<span class="n">s</span> <span class="o">=</span> <span class="n">s</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"&quot;"</span><span class="p">,</span> <span class="s1">'</span><span class="se">\\</span><span class="s1">"'</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">html</span><span class="o">.</span><span class="n">unescape</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="nd">@atexit</span><span class="o">.</span><span class="n">register</span>
|
||||
<span class="k">def</span> <span class="nf">terminate_exiftool</span><span class="p">():</span>
|
||||
<span class="sd">"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""</span>
|
||||
<span class="sd">"""Terminate any running ExifTool subprocesses; call this to cleanup when done using ExifTool"""</span>
|
||||
<span class="k">for</span> <span class="n">proc</span> <span class="ow">in</span> <span class="n">EXIFTOOL_PROCESSES</span><span class="p">:</span>
|
||||
<span class="n">proc</span><span class="o">.</span><span class="n">_stop_proc</span><span class="p">()</span>
|
||||
|
||||
|
||||
<span class="nd">@lru_cache</span><span class="p">(</span><span class="n">maxsize</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
|
||||
<span class="k">def</span> <span class="nf">get_exiftool_path</span><span class="p">():</span>
|
||||
<span class="sd">"""return path of exiftool, cache result"""</span>
|
||||
<span class="k">if</span> <span class="n">exiftool_path</span> <span class="o">:=</span> <span class="n">shutil</span><span class="o">.</span><span class="n">which</span><span class="p">(</span><span class="s2">"exiftool"</span><span class="p">):</span>
|
||||
<span class="sd">"""return path of exiftool, cache result"""</span>
|
||||
<span class="k">if</span> <span class="n">exiftool_path</span> <span class="o">:=</span> <span class="n">shutil</span><span class="o">.</span><span class="n">which</span><span class="p">(</span><span class="s2">"exiftool"</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">exiftool_path</span><span class="o">.</span><span class="n">rstrip</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span>
|
||||
<span class="s2">"Could not find exiftool. Please download and install from "</span>
|
||||
<span class="s2">"https://exiftool.org/"</span>
|
||||
<span class="s2">"Could not find exiftool. Please download and install from "</span>
|
||||
<span class="s2">"https://exiftool.org/"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">_ExifToolProc</span><span class="p">:</span>
|
||||
<span class="sd">"""Runs exiftool in a subprocess via Popen</span>
|
||||
<span class="sd"> Creates a singleton object"""</span>
|
||||
<span class="sd">"""Runs exiftool in a subprocess via Popen</span>
|
||||
<span class="sd"> Creates a singleton object"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="s2">"instance"</span><span class="p">)</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="s2">"instance"</span><span class="p">)</span> <span class="ow">or</span> <span class="ow">not</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span><span class="p">:</span>
|
||||
<span class="bp">cls</span><span class="o">.</span><span class="n">instance</span> <span class="o">=</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="bp">cls</span><span class="o">.</span><span class="n">instance</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">large_file_support</span><span class="o">=</span><span class="kc">True</span><span class="p">):</span>
|
||||
<span class="sd">"""construct _ExifToolProc singleton object or return instance of already created object</span>
|
||||
<span class="sd">"""construct _ExifToolProc singleton object or return instance of already created object</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> exiftool: optional path to exiftool binary (if not provided, will search path to find it)</span>
|
||||
<span class="sd"> large_file_support: if True, enables large file support (>4GB) via `-api largefilesupport=1`</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process_running"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process_running"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="c1"># already running</span>
|
||||
<span class="k">if</span> <span class="n">exiftool</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="n">exiftool</span> <span class="o">!=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"exiftool subprocess already running, "</span>
|
||||
<span class="sa">f</span><span class="s2">"ignoring exiftool=</span><span class="si">{</span><span class="n">exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="sa">f</span><span class="s2">"exiftool subprocess already running, "</span>
|
||||
<span class="sa">f</span><span class="s2">"ignoring exiftool=</span><span class="si">{</span><span class="n">exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_large_file_support</span> <span class="o">=</span> <span class="n">large_file_support</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span> <span class="o">=</span> <span class="n">exiftool</span> <span class="ow">or</span> <span class="n">get_exiftool_path</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">(</span><span class="n">large_file_support</span><span class="o">=</span><span class="n">large_file_support</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">process</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return the exiftool subprocess"""</span>
|
||||
<span class="sd">"""return the exiftool subprocess"""</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">()</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_start_proc</span><span class="p">(</span><span class="n">large_file_support</span><span class="o">=</span><span class="bp">self</span><span class="o">.</span><span class="n">_large_file_support</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">pid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">pid</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">exiftool</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path to exiftool process"""</span>
|
||||
<span class="sd">"""return path to exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_start_proc</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">large_file_support</span><span class="p">):</span>
|
||||
<span class="sd">"""start exiftool in batch mode"""</span>
|
||||
<span class="sd">"""start exiftool in batch mode"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"exiftool already running: </span><span class="si">{self._process}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">"exiftool already running: </span><span class="si">{self._process}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span>
|
||||
|
||||
<span class="c1"># open exiftool process</span>
|
||||
<span class="c1"># make sure /usr/bin at start of path so exiftool can find xattr (see #636)</span>
|
||||
<span class="n">env</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
<span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'/usr/bin/:</span><span class="si">{</span><span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span><span class="si">}</span><span class="s1">'</span>
|
||||
<span class="n">large_file_args</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-api"</span><span class="p">,</span> <span class="s2">"largefilesupport=1"</span><span class="p">]</span> <span class="k">if</span> <span class="n">large_file_support</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
<span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span> <span class="o">=</span> <span class="sa">f</span><span class="s1">'/usr/bin/:</span><span class="si">{</span><span class="n">env</span><span class="p">[</span><span class="s2">"PATH"</span><span class="p">]</span><span class="si">}</span><span class="s1">'</span>
|
||||
<span class="n">large_file_args</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"-api"</span><span class="p">,</span> <span class="s2">"largefilesupport=1"</span><span class="p">]</span> <span class="k">if</span> <span class="n">large_file_support</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span> <span class="o">=</span> <span class="n">subprocess</span><span class="o">.</span><span class="n">Popen</span><span class="p">(</span>
|
||||
<span class="p">[</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_exiftool</span><span class="p">,</span>
|
||||
<span class="s2">"-stay_open"</span><span class="p">,</span> <span class="c1"># keep process open in batch mode</span>
|
||||
<span class="s2">"True"</span><span class="p">,</span> <span class="c1"># -stay_open=True, keep process open in batch mode</span>
|
||||
<span class="s2">"-stay_open"</span><span class="p">,</span> <span class="c1"># keep process open in batch mode</span>
|
||||
<span class="s2">"True"</span><span class="p">,</span> <span class="c1"># -stay_open=True, keep process open in batch mode</span>
|
||||
<span class="o">*</span><span class="n">large_file_args</span><span class="p">,</span>
|
||||
<span class="s2">"-@"</span><span class="p">,</span> <span class="c1"># read command-line arguments from file</span>
|
||||
<span class="s2">"-"</span><span class="p">,</span> <span class="c1"># read from stdin</span>
|
||||
<span class="s2">"-common_args"</span><span class="p">,</span> <span class="c1"># specifies args common to all commands subsequently run</span>
|
||||
<span class="s2">"-n"</span><span class="p">,</span> <span class="c1"># no print conversion (e.g. print tag values in machine readable format)</span>
|
||||
<span class="s2">"-P"</span><span class="p">,</span> <span class="c1"># Preserve file modification date/time</span>
|
||||
<span class="s2">"-G"</span><span class="p">,</span> <span class="c1"># print group name for each tag</span>
|
||||
<span class="s2">"-E"</span><span class="p">,</span> <span class="c1"># escape tag values for HTML (allows use of HTML &#xa; for newlines)</span>
|
||||
<span class="s2">"-@"</span><span class="p">,</span> <span class="c1"># read command-line arguments from file</span>
|
||||
<span class="s2">"-"</span><span class="p">,</span> <span class="c1"># read from stdin</span>
|
||||
<span class="s2">"-common_args"</span><span class="p">,</span> <span class="c1"># specifies args common to all commands subsequently run</span>
|
||||
<span class="s2">"-n"</span><span class="p">,</span> <span class="c1"># no print conversion (e.g. print tag values in machine readable format)</span>
|
||||
<span class="s2">"-P"</span><span class="p">,</span> <span class="c1"># Preserve file modification date/time</span>
|
||||
<span class="s2">"-G"</span><span class="p">,</span> <span class="c1"># print group name for each tag</span>
|
||||
<span class="s2">"-E"</span><span class="p">,</span> <span class="c1"># escape tag values for HTML (allows use of HTML &#xa; for newlines)</span>
|
||||
<span class="p">],</span>
|
||||
<span class="n">stdin</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
|
||||
<span class="n">stdout</span><span class="o">=</span><span class="n">subprocess</span><span class="o">.</span><span class="n">PIPE</span><span class="p">,</span>
|
||||
@@ -375,14 +377,14 @@
|
||||
<span class="n">EXIFTOOL_PROCESSES</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_stop_proc</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""stop the exiftool process if it's running, otherwise, do nothing"""</span>
|
||||
<span class="sd">"""stop the exiftool process if it's running, otherwise, do nothing"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process_running</span><span class="p">:</span>
|
||||
<span class="k">return</span>
|
||||
|
||||
<span class="k">with</span> <span class="n">contextlib</span><span class="o">.</span><span class="n">suppress</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"-stay_open</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"False</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"-stay_open</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="sa">b</span><span class="s2">"False</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">communicate</span><span class="p">(</span><span class="n">timeout</span><span class="o">=</span><span class="mi">5</span><span class="p">)</span>
|
||||
@@ -395,7 +397,7 @@
|
||||
|
||||
|
||||
<div class="viewcode-block" id="ExifTool"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool">[docs]</a><span class="k">class</span> <span class="nc">ExifTool</span><span class="p">:</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags"""</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="p">,</span>
|
||||
@@ -405,7 +407,7 @@
|
||||
<span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">large_file_support</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span>
|
||||
<span class="p">):</span>
|
||||
<span class="sd">"""Create ExifTool object</span>
|
||||
<span class="sd">"""Create ExifTool object</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> file: path to image file</span>
|
||||
@@ -416,7 +418,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ExifTool instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">file</span> <span class="o">=</span> <span class="n">filepath</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="o">=</span> <span class="n">overwrite</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">flags</span> <span class="o">=</span> <span class="n">flags</span> <span class="ow">or</span> <span class="p">[]</span>
|
||||
@@ -435,7 +437,7 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">process</span>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.setvalue"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.setvalue">[docs]</a> <span class="k">def</span> <span class="nf">setvalue</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="sd">"""Set tag to value(s); if value is None, will delete tag</span>
|
||||
<span class="sd">"""Set tag to value(s); if value is None, will delete tag</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag: str; name of tag to set</span>
|
||||
@@ -447,27 +449,27 @@
|
||||
<span class="sd"> If error generated by exiftool, returns False and sets self.error to error string</span>
|
||||
<span class="sd"> If warning generated by exiftool, returns True (unless there was also an error) and sets self.warning to warning string</span>
|
||||
<span class="sd"> If called in context manager, returns True (execution is delayed until exiting context manager)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="s2">""</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="s2">""</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">escape_str</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span>
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># avoid "Warning: Some character(s) could not be encoded in Latin" warning</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-iptc:codedcharacterset=utf8"</span><span class="p">)</span>
|
||||
<span class="c1"># avoid "Warning: Some character(s) could not be encoded in Latin" warning</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-iptc:codedcharacterset=utf8"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_commands</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">True</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.addvalues"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.addvalues">[docs]</a> <span class="k">def</span> <span class="nf">addvalues</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="o">*</span><span class="n">values</span><span class="p">):</span>
|
||||
<span class="sd">"""Add one or more value(s) to tag</span>
|
||||
<span class="sd">"""Add one or more value(s) to tag</span>
|
||||
<span class="sd"> If more than one value is passed, each value will be added to the tag</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
@@ -485,32 +487,32 @@
|
||||
<span class="sd"> the values being added are not already in the EXIF data</span>
|
||||
<span class="sd"> For some tags, such as IPTC:Keywords, this will add a new value to the list of keywords,</span>
|
||||
<span class="sd"> but for others, such as EXIF:ISO, this will literally add a value to the existing value.</span>
|
||||
<span class="sd"> It's up to the caller to know what exiftool will do for each tag</span>
|
||||
<span class="sd"> It's up to the caller to know what exiftool will do for each tag</span>
|
||||
<span class="sd"> If setvalue called before addvalues, exiftool does not appear to add duplicates,</span>
|
||||
<span class="sd"> but if addvalues called without first calling setvalue, exiftool will add duplicate values</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Must pass at least one value"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Must pass at least one value"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">command</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">value</span> <span class="ow">in</span> <span class="n">values</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">value</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Can't add None value to tag"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"Can't add None value to tag"</span><span class="p">)</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">escape_str</span><span class="p">(</span><span class="n">value</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">+=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s2">"-</span><span class="si">{</span><span class="n">tag</span><span class="si">}</span><span class="s2">+=</span><span class="si">{</span><span class="n">value</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">command</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_commands</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">True</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">error</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">command</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
<span class="k">return</span> <span class="n">error</span> <span class="o">==</span> <span class="s2">""</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.run_commands"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.run_commands">[docs]</a> <span class="k">def</span> <span class="nf">run_commands</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""Run commands in the exiftool process and return result.</span>
|
||||
<span class="sd">"""Run commands in the exiftool process and return result.</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> *commands: exiftool commands to run</span>
|
||||
@@ -524,35 +526,35 @@
|
||||
<span class="sd"> error: if exiftool generated errors, string containing otherwise empty string</span>
|
||||
|
||||
<span class="sd"> Note: Also sets self.warning and self.error if warning or error generated.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"exiftool process is not running"</span><span class="p">)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="p">(</span><span class="nb">hasattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="s2">"_process"</span><span class="p">)</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">"exiftool process is not running"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">commands</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"must provide one or more command to run"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">TypeError</span><span class="p">(</span><span class="s2">"must provide one or more command to run"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span> <span class="ow">and</span> <span class="bp">self</span><span class="o">.</span><span class="n">overwrite</span><span class="p">:</span>
|
||||
<span class="n">commands</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">commands</span><span class="p">)</span>
|
||||
<span class="n">commands</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
<span class="n">commands</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="s2">"-overwrite_original"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">if</span> <span class="n">no_file</span> <span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">fsencode</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="p">)</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">if</span> <span class="n">no_file</span> <span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">fsencode</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">flags</span><span class="p">:</span>
|
||||
<span class="c1"># need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]</span>
|
||||
<span class="c1"># need to split flags, e.g. so "--ext AVI" becomes ["--ext", "AVI"]</span>
|
||||
<span class="n">flags</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">flags</span><span class="p">:</span>
|
||||
<span class="n">flags</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">f</span><span class="o">.</span><span class="n">split</span><span class="p">())</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">f</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">flags</span><span class="p">])</span>
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">f</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">flags</span><span class="p">])</span>
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">command_str</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
|
||||
<span class="n">command_str</span> <span class="o">+=</span> <span class="p">(</span>
|
||||
<span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">c</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">commands</span><span class="p">])</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="o">.</span><span class="n">join</span><span class="p">([</span><span class="n">c</span><span class="o">.</span><span class="n">encode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">commands</span><span class="p">])</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="n">filename</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"-execute</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="o">+</span> <span class="sa">b</span><span class="s2">"-execute</span><span class="se">\n</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># send the command</span>
|
||||
@@ -560,19 +562,19 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdin</span><span class="o">.</span><span class="n">flush</span><span class="p">()</span>
|
||||
|
||||
<span class="c1"># read the output</span>
|
||||
<span class="n">output</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">output</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="sa">b</span><span class="s2">""</span>
|
||||
<span class="k">while</span> <span class="n">EXIFTOOL_STAYOPEN_EOF</span> <span class="ow">not</span> <span class="ow">in</span> <span class="nb">str</span><span class="p">(</span><span class="n">output</span><span class="p">):</span>
|
||||
<span class="n">line</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">readline</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Warning"</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Warning"</span><span class="p">):</span>
|
||||
<span class="n">warning</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">elif</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Error"</span><span class="p">):</span>
|
||||
<span class="k">elif</span> <span class="n">line</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="sa">b</span><span class="s2">"Error"</span><span class="p">):</span>
|
||||
<span class="n">error</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">output</span> <span class="o">+=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">warning</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">warning</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">error</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">error</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">warning</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">warning</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">warning</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="n">error</span> <span class="o">=</span> <span class="s2">""</span> <span class="k">if</span> <span class="n">error</span> <span class="o">==</span> <span class="sa">b</span><span class="s2">""</span> <span class="k">else</span> <span class="n">error</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">warning</span> <span class="o">=</span> <span class="n">warning</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">error</span> <span class="o">=</span> <span class="n">error</span>
|
||||
|
||||
@@ -580,41 +582,41 @@
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">pid</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="sd">"""return process id (PID) of the exiftool process"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_process</span><span class="o">.</span><span class="n">pid</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">version</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns exiftool version"""</span>
|
||||
<span class="n">ver</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-ver"</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">ver</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
<span class="sd">"""returns exiftool version"""</span>
|
||||
<span class="n">ver</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-ver"</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">ver</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">)</span>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag_groups</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd"> returns empty dict if no tags</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> normalized: if True, dict keys are all normalized to lower case (default is False)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">json_str</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">json_str</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">json_str</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="nb">dict</span><span class="p">()</span>
|
||||
<span class="n">json_str</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json_str</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="n">json_str</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json_str</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">json_str</span><span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="c1"># will fail with some commands, e.g --ext AVI which produces</span>
|
||||
<span class="c1"># 'No file with specified extension' instead of json</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"error loading json returned by exiftool: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">json_str</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="c1"># 'No file with specified extension' instead of json</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"error loading json returned by exiftool: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">json_str</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="nb">dict</span><span class="p">()</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exifdict</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">tag_groups</span><span class="p">:</span>
|
||||
<span class="c1"># strip tag groups</span>
|
||||
<span class="n">exif_new</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="k">for</span> <span class="n">k</span><span class="p">,</span> <span class="n">v</span> <span class="ow">in</span> <span class="n">exifdict</span><span class="o">.</span><span class="n">items</span><span class="p">():</span>
|
||||
<span class="n">k</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">".*:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
|
||||
<span class="n">k</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">".*:"</span><span class="p">,</span> <span class="s2">""</span><span class="p">,</span> <span class="n">k</span><span class="p">)</span>
|
||||
<span class="n">exif_new</span><span class="p">[</span><span class="n">k</span><span class="p">]</span> <span class="o">=</span> <span class="n">v</span>
|
||||
<span class="n">exifdict</span> <span class="o">=</span> <span class="n">exif_new</span>
|
||||
|
||||
@@ -624,17 +626,17 @@
|
||||
<span class="k">return</span> <span class="n">exifdict</span></div>
|
||||
|
||||
<div class="viewcode-block" id="ExifTool.json"><a class="viewcode-back" href="../../reference.html#osxphotos.ExifTool.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns JSON string containing all EXIF tags and values from exiftool"""</span>
|
||||
<span class="n">json</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="n">json</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="sd">"""returns JSON string containing all EXIF tags and values from exiftool"""</span>
|
||||
<span class="n">json</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="s2">"-json"</span><span class="p">)</span>
|
||||
<span class="n">json</span> <span class="o">=</span> <span class="n">unescape_str</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">"utf-8"</span><span class="p">))</span>
|
||||
<span class="k">return</span> <span class="n">json</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_read_exif</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""read exif data from file"""</span>
|
||||
<span class="sd">"""read exif data from file"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">data</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">()</span><span class="o">.</span><span class="n">copy</span><span class="p">()</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"file: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="si">}</span><span class="se">\n</span><span class="s2">exiftool: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">_exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"file: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">file</span><span class="si">}</span><span class="se">\n</span><span class="s2">exiftool: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_exiftoolproc</span><span class="o">.</span><span class="n">_exiftool</span><span class="si">}</span><span class="s2">"</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_context_mgr</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
@@ -650,15 +652,15 @@
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">ExifToolCaching</span><span class="p">(</span><span class="n">ExifTool</span><span class="p">):</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags, with caching.</span>
|
||||
<span class="sd"> Use this only when you know the file's EXIF data will not be changed by any external process.</span>
|
||||
<span class="sd">"""Basic exiftool interface for reading and writing EXIF tags, with caching.</span>
|
||||
<span class="sd"> Use this only when you know the file's EXIF data will not be changed by any external process.</span>
|
||||
|
||||
<span class="sd"> Creates a singleton cached ExifTool instance"""</span>
|
||||
<span class="sd"> Creates a singleton cached ExifTool instance"""</span>
|
||||
|
||||
<span class="n">_singletons</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__new__</span><span class="p">(</span><span class="bp">cls</span><span class="p">,</span> <span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="sd">"""create new object or return instance of already created singleton"""</span>
|
||||
<span class="k">if</span> <span class="n">filepath</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">:</span>
|
||||
<span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">[</span><span class="n">filepath</span><span class="p">]</span> <span class="o">=</span> <span class="n">_ExifToolCaching</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="n">exiftool</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">cls</span><span class="o">.</span><span class="n">_singletons</span><span class="p">[</span><span class="n">filepath</span><span class="p">]</span>
|
||||
@@ -666,7 +668,7 @@
|
||||
|
||||
<span class="k">class</span> <span class="nc">_ExifToolCaching</span><span class="p">(</span><span class="n">ExifTool</span><span class="p">):</span>
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""Create read-only ExifTool object that caches values</span>
|
||||
<span class="sd">"""Create read-only ExifTool object that caches values</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> file: path to image file</span>
|
||||
@@ -674,21 +676,21 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> ExifTool instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
<span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">exiftool</span><span class="o">=</span><span class="n">exiftool</span><span class="p">,</span> <span class="n">overwrite</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="n">flags</span><span class="o">=</span><span class="kc">None</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">run_commands</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">commands</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"-json"</span><span class="p">,</span> <span class="s2">"-ver"</span><span class="p">]:</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">commands</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="ow">not</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"-json"</span><span class="p">,</span> <span class="s2">"-ver"</span><span class="p">]:</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="n">run_commands</span><span class="p">(</span><span class="o">*</span><span class="n">commands</span><span class="p">,</span> <span class="n">no_file</span><span class="o">=</span><span class="n">no_file</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">setvalue</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="n">value</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">addvalues</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag</span><span class="p">,</span> <span class="o">*</span><span class="n">values</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">NotImplementedError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="vm">__class__</span><span class="si">}</span><span class="s2"> is read-only"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span><span class="p">:</span>
|
||||
@@ -696,13 +698,13 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">tag_groups</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">normalized</span><span class="o">=</span><span class="kc">False</span><span class="p">):</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd">"""return dictionary of all EXIF tags and values from exiftool</span>
|
||||
<span class="sd"> returns empty dict if no tags</span>
|
||||
|
||||
<span class="sd"> Args:</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> tag_groups: if True (default), dict keys have tag groups, e.g. "IPTC:Keywords"; if False, drops groups from keys, e.g. "Keywords"</span>
|
||||
<span class="sd"> normalized: if True, dict keys are all normalized to lower case (default is False)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span><span class="p">[</span><span class="n">tag_groups</span><span class="p">][</span><span class="n">normalized</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
@@ -714,7 +716,7 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span><span class="p">[</span><span class="n">tag_groups</span><span class="p">][</span><span class="n">normalized</span><span class="p">]</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">flush_cache</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Clear cached data so that calls to json or asdict return fresh data"""</span>
|
||||
<span class="sd">"""Clear cached data so that calls to json or asdict return fresh data"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_json_cache</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_asdict_cache</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
</pre></div>
|
||||
@@ -754,7 +756,9 @@
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
|
||||
<title>osxphotos.personinfo - osxphotos 0.50.4 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.personinfo - osxphotos 0.56.5 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.50.4 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.5 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.50.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.5 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -179,7 +179,8 @@
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container"><div class="theme-toggle-container theme-toggle-content">
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
@@ -194,25 +195,27 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.personinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" PhotoInfo and FaceInfo classes to expose info about persons and faces in the Photos library """</span>
|
||||
<span></span><span class="sd">""" PhotoInfo and FaceInfo classes to expose info about persons and faces in the Photos library """</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">json</span>
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
<span class="kn">import</span> <span class="nn">math</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">namedtuple</span>
|
||||
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cached_property</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PersonInfo"</span><span class="p">,</span> <span class="s2">"FaceInfo"</span><span class="p">,</span> <span class="s2">"rotate_image_point"</span><span class="p">]</span>
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
|
||||
<span class="n">MWG_RS_Area</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MWG_RS_Area"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
|
||||
<span class="n">MPRI_Reg_Rect</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MPRI_Reg_Rect"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PersonInfo"</span><span class="p">,</span> <span class="s2">"FaceInfo"</span><span class="p">,</span> <span class="s2">"rotate_image_point"</span><span class="p">]</span>
|
||||
|
||||
<span class="n">MWG_RS_Area</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MWG_RS_Area"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
|
||||
<span class="n">MPRI_Reg_Rect</span> <span class="o">=</span> <span class="n">namedtuple</span><span class="p">(</span><span class="s2">"MPRI_Reg_Rect"</span><span class="p">,</span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">,</span> <span class="s2">"h"</span><span class="p">,</span> <span class="s2">"w"</span><span class="p">])</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="PersonInfo"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo">[docs]</a><span class="k">class</span> <span class="nc">PersonInfo</span><span class="p">:</span>
|
||||
<span class="sd">"""Info about a person in the Photos library"""</span>
|
||||
<span class="sd">"""Info about a person in the Photos library"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""Creates a new PersonInfo instance</span>
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">:</span> <span class="s2">"osxphotos.PhotosDB"</span><span class="p">,</span> <span class="n">pk</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
|
||||
<span class="sd">"""Creates a new PersonInfo instance</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> db: instance of PhotosDB object</span>
|
||||
@@ -220,16 +223,16 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> PersonInfo instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_pk</span> <span class="o">=</span> <span class="n">pk</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">:</span> <span class="s2">"osxphotos.PhotosDB"</span> <span class="o">=</span> <span class="n">db</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">:</span> <span class="nb">int</span> <span class="o">=</span> <span class="n">pk</span>
|
||||
|
||||
<span class="n">person</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">display_name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"displayname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">keyface</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"keyface"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">facecount</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"facecount"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">display_name</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"displayname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">keyface</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"keyface"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">facecount</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="s2">"facecount"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">keyphoto</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
@@ -237,9 +240,9 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_keyphoto</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="n">person</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">]:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">key_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">])</span>
|
||||
<span class="n">key_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="s2">"photo_uuid"</span><span class="p">])</span>
|
||||
<span class="k">except</span> <span class="ne">IndexError</span><span class="p">:</span>
|
||||
<span class="n">key_photo</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
@@ -249,14 +252,14 @@
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">photos</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns list of PhotoInfo objects associated with this person"""</span>
|
||||
<span class="sd">"""Returns list of PhotoInfo objects associated with this person"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_by_uuid</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbfaces_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">])</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">face_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns a list of FaceInfo objects associated with this person sorted by quality score</span>
|
||||
<span class="sd">"""Returns a list of FaceInfo objects associated with this person sorted by quality score</span>
|
||||
<span class="sd"> Highest quality face is result[0] and lowest quality face is result[n]</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">faces</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_faceinfo_person</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">]</span>
|
||||
<span class="k">return</span> <span class="nb">sorted</span><span class="p">(</span>
|
||||
@@ -268,30 +271,60 @@
|
||||
<span class="c1"># no faces</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">favorite</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns True if person is a favorite, False otherwise; Photos 5+ only; returns False on Photos <= 4"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">sort_order</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns sort order of person; favorite persons are sorted before non-favorite persons"; Photos 5+ only; returns 0 on Photos <= 4"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">][</span><span class="s2">"manualorder"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@cached_property</span>
|
||||
<span class="k">def</span> <span class="nf">feature_less</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns True if person has been marked as "Feature This Person Less" in Photos, False otherwise; Photos 8+ only; returns False on Photos <= 7"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">photos_version</span> <span class="o"><</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">results</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> SELECT ZTYPE</span>
|
||||
<span class="sd"> FROM ZUSERFEEDBACK</span>
|
||||
<span class="sd"> WHERE ZPERSON = ?</span>
|
||||
<span class="sd"> """</span><span class="p">,</span>
|
||||
<span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">,),</span>
|
||||
<span class="p">)</span><span class="o">.</span><span class="n">fetchone</span><span class="p">():</span>
|
||||
<span class="k">return</span> <span class="nb">bool</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
|
||||
<div class="viewcode-block" id="PersonInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns dictionary representation of class instance"""</span>
|
||||
<span class="sd">"""Returns dictionary representation of class instance"""</span>
|
||||
<span class="n">keyphoto</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyphoto</span><span class="o">.</span><span class="n">uuid</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyphoto</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="kc">None</span>
|
||||
<span class="k">return</span> <span class="p">{</span>
|
||||
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||
<span class="s2">"displayname"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="p">,</span>
|
||||
<span class="s2">"keyface"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyface</span><span class="p">,</span>
|
||||
<span class="s2">"facecount"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="p">,</span>
|
||||
<span class="s2">"keyphoto"</span><span class="p">:</span> <span class="n">keyphoto</span><span class="p">,</span>
|
||||
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||
<span class="s2">"displayname"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="p">,</span>
|
||||
<span class="s2">"keyface"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">keyface</span><span class="p">,</span>
|
||||
<span class="s2">"facecount"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="p">,</span>
|
||||
<span class="s2">"keyphoto"</span><span class="p">:</span> <span class="n">keyphoto</span><span class="p">,</span>
|
||||
<span class="s2">"favorite"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">favorite</span><span class="p">,</span>
|
||||
<span class="s2">"sort_order"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">sort_order</span><span class="p">,</span>
|
||||
<span class="s2">"feature_less"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">feature_less</span><span class="p">,</span>
|
||||
<span class="p">}</span></div>
|
||||
|
||||
<div class="viewcode-block" id="PersonInfo.json"><a class="viewcode-back" href="../../reference.html#osxphotos.PersonInfo.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns JSON representation of class instance"""</span>
|
||||
<span class="sd">"""Returns JSON representation of class instance"""</span>
|
||||
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">())</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"PersonInfo(name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, display_name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="si">}</span><span class="s2">, uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, facecount=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"PersonInfo(name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, display_name=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">display_name</span><span class="si">}</span><span class="s2">, uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, facecount=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">facecount</span><span class="si">}</span><span class="s2">)"</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="nb">all</span><span class="p">(</span>
|
||||
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="bp">self</span><span class="p">))</span>
|
||||
<span class="k">else</span> <span class="kc">False</span>
|
||||
@@ -302,10 +335,10 @@
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">FaceInfo</span><span class="p">:</span>
|
||||
<span class="sd">"""Info about a face in the Photos library"""</span>
|
||||
<span class="sd">"""Info about a face in the Photos library"""</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">pk</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="sd">"""Creates a new FaceInfo instance</span>
|
||||
<span class="sd">"""Creates a new FaceInfo instance</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> db: instance of PhotosDB object</span>
|
||||
@@ -313,59 +346,59 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> FaceInfo instance</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_pk</span> <span class="o">=</span> <span class="n">pk</span>
|
||||
|
||||
<span class="n">face</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_faceinfo_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">face</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"asset_uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"person"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">center_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centerx"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">center_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centery"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"size"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">quality</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"quality"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">source_width</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourcewidth"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">source_height</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourceheight"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"has_smile"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">manual</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"manual"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">face_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facetype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">age_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"agetype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyemakeuptype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyestate"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facialhairtype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"gendertype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"glassestype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"haircolortype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">intrash</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"intrash"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"lipmakeuptype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"smiletype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"fullname"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"asset_uuid"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"person"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">center_x</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centerx"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">center_y</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"centery"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"size"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">quality</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"quality"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">source_width</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourcewidth"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">source_height</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"sourceheight"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"has_smile"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">manual</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"manual"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">face_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facetype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">age_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"agetype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyemakeuptype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"eyestate"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"facialhairtype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"gendertype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"glassestype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"haircolortype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">intrash</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"intrash"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"lipmakeuptype"</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span> <span class="o">=</span> <span class="n">face</span><span class="p">[</span><span class="s2">"smiletype"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">center</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Coordinates, in PIL format, for center of face</span>
|
||||
<span class="sd">"""Coordinates, in PIL format, for center of face</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> tuple of coordinates in form (x, y)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point</span><span class="p">((</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">))</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">size_pixels</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Size of face in pixels (centered around center_x, center_y)</span>
|
||||
<span class="sd">"""Size of face in pixels (centered around center_x, center_y)</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> size, in int pixels, of a circle drawn around the center of the face</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span>
|
||||
<span class="n">size_reference</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="o">></span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span> <span class="k">else</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">*</span> <span class="n">size_reference</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">person_info</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""PersonInfo instance for person associated with this face"""</span>
|
||||
<span class="sd">"""PersonInfo instance for person associated with this face"""</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
@@ -374,18 +407,18 @@
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""PhotoInfo instance associated with this face"""</span>
|
||||
<span class="sd">"""PhotoInfo instance associated with this face"""</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_photo</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not get photo for uuid: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not get photo for uuid: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">mwg_rs_area</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Get coordinates for Metadata Working Group Region Area.</span>
|
||||
<span class="sd">"""Get coordinates for Metadata Working Group Region Area.</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> MWG_RS_Area named tuple with x, y, h, w where:</span>
|
||||
@@ -396,7 +429,7 @@
|
||||
|
||||
<span class="sd"> Reference:</span>
|
||||
<span class="sd"> https://photo.stackexchange.com/questions/106410/how-does-xmp-define-the-face-region</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_fix_orientation</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
|
||||
|
||||
@@ -411,7 +444,7 @@
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">mpri_reg_rect</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Get coordinates for Microsoft Photo Region Rectangle.</span>
|
||||
<span class="sd">"""Get coordinates for Microsoft Photo Region Rectangle.</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> MPRI_Reg_Rect named tuple with x, y, h, w where:</span>
|
||||
@@ -422,7 +455,7 @@
|
||||
|
||||
<span class="sd"> Reference:</span>
|
||||
<span class="sd"> https://docs.microsoft.com/en-us/windows/win32/wic/-wic-people-tagging</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_fix_orientation</span><span class="p">((</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">))</span>
|
||||
|
||||
@@ -440,13 +473,13 @@
|
||||
<span class="k">return</span> <span class="n">MPRI_Reg_Rect</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">h</span><span class="p">,</span> <span class="n">w</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">face_rect</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Get face rectangle coordinates for current version of the associated image</span>
|
||||
<span class="sd">"""Get face rectangle coordinates for current version of the associated image</span>
|
||||
<span class="sd"> If image has been edited, rectangle applies to edited version, otherwise original version</span>
|
||||
<span class="sd"> Coordinates in format and reference frame used by PIL</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> list [(x0, x1), (y0, y1)] of coordinates in reference frame used by PIL</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="n">photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span>
|
||||
<span class="n">size_reference</span> <span class="o">=</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="k">if</span> <span class="n">photo</span><span class="o">.</span><span class="n">width</span> <span class="o">></span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span> <span class="k">else</span> <span class="n">photo</span><span class="o">.</span><span class="n">height</span>
|
||||
<span class="n">radius</span> <span class="o">=</span> <span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span> <span class="o">/</span> <span class="mi">2</span><span class="p">)</span> <span class="o">*</span> <span class="n">size_reference</span>
|
||||
@@ -456,34 +489,34 @@
|
||||
<span class="k">return</span> <span class="p">[(</span><span class="n">x0</span><span class="p">,</span> <span class="n">y0</span><span class="p">),</span> <span class="p">(</span><span class="n">x1</span><span class="p">,</span> <span class="n">y1</span><span class="p">)]</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">roll_pitch_yaw</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Roll, pitch, yaw of face in radians as tuple"""</span>
|
||||
<span class="sd">"""Roll, pitch, yaw of face in radians as tuple"""</span>
|
||||
<span class="n">info</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span>
|
||||
<span class="n">roll</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span>
|
||||
<span class="n">pitch</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span>
|
||||
<span class="n">yaw</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span>
|
||||
<span class="n">roll</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"roll"</span><span class="p">]</span>
|
||||
<span class="n">pitch</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"pitch"</span><span class="p">]</span>
|
||||
<span class="n">yaw</span> <span class="o">=</span> <span class="mi">0</span> <span class="k">if</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span> <span class="ow">is</span> <span class="kc">None</span> <span class="k">else</span> <span class="n">info</span><span class="p">[</span><span class="s2">"yaw"</span><span class="p">]</span>
|
||||
|
||||
<span class="k">return</span> <span class="p">(</span><span class="n">roll</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">yaw</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">roll</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return roll angle in radians of the face region"""</span>
|
||||
<span class="sd">"""Return roll angle in radians of the face region"""</span>
|
||||
<span class="n">roll</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="n">roll</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">pitch</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return pitch angle in radians of the face region"""</span>
|
||||
<span class="sd">"""Return pitch angle in radians of the face region"""</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="n">pitch</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">yaw</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return yaw angle in radians of the face region"""</span>
|
||||
<span class="sd">"""Return yaw angle in radians of the face region"""</span>
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">yaw</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="n">yaw</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_fix_orientation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> xy: tuple of (x, y) coordinates for point to translate</span>
|
||||
@@ -491,7 +524,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> (x, y) tuple of translated coordinates</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="c1"># Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94</span>
|
||||
|
||||
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">orientation</span>
|
||||
@@ -515,15 +548,15 @@
|
||||
<span class="k">elif</span> <span class="n">orientation</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="n">y</span><span class="p">,</span> <span class="n">x</span>
|
||||
<span class="k">elif</span> <span class="n">orientation</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
|
||||
<span class="c1"># set by osxphotos if adjusted orientation cannot be read, assume it's 1</span>
|
||||
<span class="c1"># set by osxphotos if adjusted orientation cannot be read, assume it's 1</span>
|
||||
<span class="n">y</span> <span class="o">=</span> <span class="mf">1.0</span> <span class="o">-</span> <span class="n">y</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unhandled orientation: </span><span class="si">{</span><span class="n">orientation</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unhandled orientation: </span><span class="si">{</span><span class="n">orientation</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_make_point</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation</span>
|
||||
<span class="sd"> and convert to image coordinates</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
@@ -532,7 +565,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> (x, y) tuple of translated coordinates in pixels in PIL format/reference frame</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="c1"># Reference: https://github.com/neilpa/phace/blob/7594776480505d0c389688a42099c94ac5d34f3f/cmd/phace/draw.go#L79-L94</span>
|
||||
|
||||
<span class="n">orientation</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">photo</span><span class="o">.</span><span class="n">orientation</span>
|
||||
@@ -544,7 +577,7 @@
|
||||
<span class="k">return</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="n">dx</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">y</span> <span class="o">*</span> <span class="n">dy</span><span class="p">))</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_make_point_with_rotation</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">xy</span><span class="p">):</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation and rotation</span>
|
||||
<span class="sd">"""Translate an (x, y) tuple based on image orientation and rotation</span>
|
||||
<span class="sd"> and convert to image coordinates</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
@@ -553,7 +586,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> (x, y) tuple of translated coordinates in pixels in PIL format/reference frame</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="c1"># convert to image coordinates</span>
|
||||
<span class="n">x</span><span class="p">,</span> <span class="n">y</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_make_point</span><span class="p">(</span><span class="n">xy</span><span class="p">)</span>
|
||||
@@ -566,58 +599,58 @@
|
||||
<span class="k">return</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">xr</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">yr</span><span class="p">))</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns dict representation of class instance"""</span>
|
||||
<span class="sd">"""Returns dict representation of class instance"""</span>
|
||||
<span class="n">roll</span><span class="p">,</span> <span class="n">pitch</span><span class="p">,</span> <span class="n">yaw</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">roll_pitch_yaw</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="p">{</span>
|
||||
<span class="s2">"_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">,</span>
|
||||
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||
<span class="s2">"asset_uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">,</span>
|
||||
<span class="s2">"_person_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span><span class="p">,</span>
|
||||
<span class="s2">"center_x"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span>
|
||||
<span class="s2">"center_y"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">,</span>
|
||||
<span class="s2">"center"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="p">,</span>
|
||||
<span class="s2">"size"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
|
||||
<span class="s2">"face_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_rect</span><span class="p">(),</span>
|
||||
<span class="s2">"mpri_reg_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mpri_reg_rect</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
|
||||
<span class="s2">"mwg_rs_area"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mwg_rs_area</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
|
||||
<span class="s2">"roll"</span><span class="p">:</span> <span class="n">roll</span><span class="p">,</span>
|
||||
<span class="s2">"pitch"</span><span class="p">:</span> <span class="n">pitch</span><span class="p">,</span>
|
||||
<span class="s2">"yaw"</span><span class="p">:</span> <span class="n">yaw</span><span class="p">,</span>
|
||||
<span class="s2">"quality"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">quality</span><span class="p">,</span>
|
||||
<span class="s2">"source_width"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_width</span><span class="p">,</span>
|
||||
<span class="s2">"source_height"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_height</span><span class="p">,</span>
|
||||
<span class="s2">"has_smile"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span><span class="p">,</span>
|
||||
<span class="s2">"manual"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">manual</span><span class="p">,</span>
|
||||
<span class="s2">"face_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_type</span><span class="p">,</span>
|
||||
<span class="s2">"age_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">age_type</span><span class="p">,</span>
|
||||
<span class="s2">"eye_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span><span class="p">,</span>
|
||||
<span class="s2">"eye_state"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span><span class="p">,</span>
|
||||
<span class="s2">"facial_hair_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span><span class="p">,</span>
|
||||
<span class="s2">"gender_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span><span class="p">,</span>
|
||||
<span class="s2">"glasses_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span><span class="p">,</span>
|
||||
<span class="s2">"hair_color_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span><span class="p">,</span>
|
||||
<span class="s2">"intrash"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
|
||||
<span class="s2">"lip_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span><span class="p">,</span>
|
||||
<span class="s2">"smile_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span><span class="p">,</span>
|
||||
<span class="s2">"_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="p">,</span>
|
||||
<span class="s2">"uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="p">,</span>
|
||||
<span class="s2">"name"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="p">,</span>
|
||||
<span class="s2">"asset_uuid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="p">,</span>
|
||||
<span class="s2">"_person_pk"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">_person_pk</span><span class="p">,</span>
|
||||
<span class="s2">"center_x"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="p">,</span>
|
||||
<span class="s2">"center_y"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="p">,</span>
|
||||
<span class="s2">"center"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">center</span><span class="p">,</span>
|
||||
<span class="s2">"size"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="p">,</span>
|
||||
<span class="s2">"face_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_rect</span><span class="p">(),</span>
|
||||
<span class="s2">"mpri_reg_rect"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mpri_reg_rect</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
|
||||
<span class="s2">"mwg_rs_area"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">mwg_rs_area</span><span class="o">.</span><span class="n">_asdict</span><span class="p">(),</span>
|
||||
<span class="s2">"roll"</span><span class="p">:</span> <span class="n">roll</span><span class="p">,</span>
|
||||
<span class="s2">"pitch"</span><span class="p">:</span> <span class="n">pitch</span><span class="p">,</span>
|
||||
<span class="s2">"yaw"</span><span class="p">:</span> <span class="n">yaw</span><span class="p">,</span>
|
||||
<span class="s2">"quality"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">quality</span><span class="p">,</span>
|
||||
<span class="s2">"source_width"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_width</span><span class="p">,</span>
|
||||
<span class="s2">"source_height"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source_height</span><span class="p">,</span>
|
||||
<span class="s2">"has_smile"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">has_smile</span><span class="p">,</span>
|
||||
<span class="s2">"manual"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">manual</span><span class="p">,</span>
|
||||
<span class="s2">"face_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">face_type</span><span class="p">,</span>
|
||||
<span class="s2">"age_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">age_type</span><span class="p">,</span>
|
||||
<span class="s2">"eye_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_makeup_type</span><span class="p">,</span>
|
||||
<span class="s2">"eye_state"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">eye_state</span><span class="p">,</span>
|
||||
<span class="s2">"facial_hair_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">facial_hair_type</span><span class="p">,</span>
|
||||
<span class="s2">"gender_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">gender_type</span><span class="p">,</span>
|
||||
<span class="s2">"glasses_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">glasses_type</span><span class="p">,</span>
|
||||
<span class="s2">"hair_color_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">hair_color_type</span><span class="p">,</span>
|
||||
<span class="s2">"intrash"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">intrash</span><span class="p">,</span>
|
||||
<span class="s2">"lip_makeup_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">lip_makeup_type</span><span class="p">,</span>
|
||||
<span class="s2">"smile_type"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">smile_type</span><span class="p">,</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return JSON representation of FaceInfo instance"""</span>
|
||||
<span class="sd">"""Return JSON representation of FaceInfo instance"""</span>
|
||||
<span class="k">return</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">asdict</span><span class="p">())</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, center_x=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="si">}</span><span class="s2">, center_y = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="si">}</span><span class="s2">, size=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2">, person=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, asset_uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">, center_x=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_x</span><span class="si">}</span><span class="s2">, center_y = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">center_y</span><span class="si">}</span><span class="s2">, size=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">size</span><span class="si">}</span><span class="s2">, person=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">name</span><span class="si">}</span><span class="s2">, asset_uuid=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">asset_uuid</span><span class="si">}</span><span class="s2">)"</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(db=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="si">}</span><span class="s2">, pk=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="si">}</span><span class="s2">)"</span>
|
||||
<span class="k">return</span> <span class="sa">f</span><span class="s2">"FaceInfo(db=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="si">}</span><span class="s2">, pk=</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_pk</span><span class="si">}</span><span class="s2">)"</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__eq__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="nb">type</span><span class="p">(</span><span class="bp">self</span><span class="p">)):</span>
|
||||
<span class="k">return</span> <span class="kc">False</span>
|
||||
|
||||
<span class="k">return</span> <span class="nb">all</span><span class="p">(</span>
|
||||
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
|
||||
<span class="nb">getattr</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="o">==</span> <span class="nb">getattr</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="p">[</span><span class="s2">"_db"</span><span class="p">,</span> <span class="s2">"_pk"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__ne__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">other</span><span class="p">):</span>
|
||||
@@ -625,7 +658,7 @@
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">rotate_image_point</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">xmid</span><span class="p">,</span> <span class="n">ymid</span><span class="p">,</span> <span class="n">angle</span><span class="p">):</span>
|
||||
<span class="sd">"""rotate image point about xm, ym by angle in radians</span>
|
||||
<span class="sd">"""rotate image point about xm, ym by angle in radians</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> x: x coordinate of point to rotate</span>
|
||||
@@ -637,7 +670,7 @@
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> tuple of rotated points (xr, yr)</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="c1"># translate point relative to the mid point</span>
|
||||
<span class="n">x</span> <span class="o">=</span> <span class="n">x</span> <span class="o">-</span> <span class="n">xmid</span>
|
||||
<span class="n">y</span> <span class="o">=</span> <span class="n">y</span> <span class="o">-</span> <span class="n">ymid</span>
|
||||
@@ -688,7 +721,9 @@
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.photoinfo - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.photoinfo - osxphotos 0.57.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.57.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -160,7 +160,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -195,28 +195,30 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.photoinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">"""</span>
|
||||
<span class="sd">PhotoInfo class</span>
|
||||
<span class="sd">Represents a single photo in the Photos library and provides access to the photo's attributes</span>
|
||||
<span></span><span class="sd">""" PhotoInfo class: Represents a single photo in the Photos library and provides access to the photo's attributes</span>
|
||||
<span class="sd">PhotosDB.photos() returns a list of PhotoInfo objects</span>
|
||||
<span class="sd">"""</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">contextlib</span>
|
||||
<span class="kn">import</span> <span class="nn">dataclasses</span>
|
||||
<span class="kn">import</span> <span class="nn">datetime</span>
|
||||
<span class="kn">import</span> <span class="nn">json</span>
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
<span class="kn">import</span> <span class="nn">os</span>
|
||||
<span class="kn">import</span> <span class="nn">os.path</span>
|
||||
<span class="kn">import</span> <span class="nn">pathlib</span>
|
||||
<span class="kn">import</span> <span class="nn">plistlib</span>
|
||||
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||||
<span class="kn">from</span> <span class="nn">functools</span> <span class="kn">import</span> <span class="n">cached_property</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Optional</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">Dict</span><span class="p">,</span> <span class="n">Optional</span>
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">yaml</span>
|
||||
<span class="kn">from</span> <span class="nn">osxmetadata</span> <span class="kn">import</span> <span class="n">OSXMetaData</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">osxphotos</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_DB_TABLE_NAMES</span><span class="p">,</span>
|
||||
<span class="n">_MOVIE_TYPE</span><span class="p">,</span>
|
||||
@@ -231,8 +233,11 @@
|
||||
<span class="n">_PHOTOS_5_IMPORT_SESSION_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_PROJECT_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_ALBUM_KIND</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_VERSION</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="n">BURST_DEFAULT_PICK</span><span class="p">,</span>
|
||||
<span class="n">BURST_KEY</span><span class="p">,</span>
|
||||
<span class="n">BURST_NOT_SELECTED</span><span class="p">,</span>
|
||||
@@ -260,6 +265,8 @@
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PhotoInfo"</span><span class="p">,</span> <span class="s2">"PhotoInfoNone"</span><span class="p">]</span>
|
||||
|
||||
<span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">"osxphotos"</span><span class="p">)</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="PhotoInfo"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo">[docs]</a><span class="k">class</span> <span class="nc">PhotoInfo</span><span class="p">:</span>
|
||||
<span class="sd">"""</span>
|
||||
@@ -267,10 +274,10 @@
|
||||
<span class="sd"> including keywords, persons, albums, uuid, path, etc.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">uuid</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span> <span class="n">info</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span> <span class="o">=</span> <span class="n">uuid</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span> <span class="o">=</span> <span class="n">info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span> <span class="o">=</span> <span class="n">db</span>
|
||||
<span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">:</span> <span class="s2">"osxphotos.PhotosDB"</span><span class="p">,</span> <span class="n">uuid</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">info</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">:</span> <span class="nb">str</span> <span class="o">=</span> <span class="n">uuid</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">Any</span><span class="p">]</span> <span class="o">=</span> <span class="n">info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="p">:</span> <span class="s2">"osxphotos.PhotosDB"</span> <span class="o">=</span> <span class="n">db</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_verbose</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
@@ -344,38 +351,59 @@
|
||||
<span class="k">return</span> <span class="n">photopath</span> <span class="c1"># path would be meaningless until downloaded</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_4</span><span class="p">()</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"shared"</span><span class="p">]:</span>
|
||||
<span class="c1"># shared photo</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">]</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"/"</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_4</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_5</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns candidate path for original photo on Photos >= version 5"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"shared"</span><span class="p">]:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_5_shared</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="p">(</span>
|
||||
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">])</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">]</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"/"</span><span class="p">)</span>
|
||||
<span class="k">else</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_5_shared</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns candidate path for shared photo on Photos >= version 5"""</span>
|
||||
<span class="c1"># shared library path differs on Photos 5-7, Photos 8+</span>
|
||||
<span class="n">shared_path</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_PHOTO_PATH</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">>=</span> <span class="mi">8</span>
|
||||
<span class="k">else</span> <span class="n">_PHOTOS_5_SHARED_PHOTO_PATH</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">isphoto</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">shared_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"filename"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># a shared video has two files, the poster image and the video</span>
|
||||
<span class="c1"># the poster (image frame shown in Photos) is named UUID.poster.JPG</span>
|
||||
<span class="c1"># the video file is named UUID.medium.MP4</span>
|
||||
<span class="c1"># this method returns the path to the video file</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">.medium.MP4"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">,</span>
|
||||
<span class="n">shared_path</span><span class="p">,</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">],</span>
|
||||
<span class="n">filename</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path for photo on Photos <= version 4"""</span>
|
||||
<span class="sd">"""Returns candidate path for original photo on Photos <= version 4"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"has_raw"</span><span class="p">]:</span>
|
||||
<span class="c1"># return the path to JPEG even if RAW is original</span>
|
||||
<span class="n">vol</span> <span class="o">=</span> <span class="p">(</span>
|
||||
@@ -400,9 +428,6 @@
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"imagePath"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
@@ -414,14 +439,20 @@
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4</span><span class="p">()</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5</span><span class="p">()</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5</span><span class="p">()</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span> <span class="o">=</span> <span class="n">photopath</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_5</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited for Photos >= 5"""</span>
|
||||
<span class="sd">"""Returns candidate path_edited for Photos >= 5 or None if cannot be determined"""</span>
|
||||
<span class="c1"># In Photos 5.0 / Catalina / MacOS 10.15:</span>
|
||||
<span class="c1"># edited photos appear to always be converted to .jpeg and stored in</span>
|
||||
<span class="c1"># library_name/resources/renders/X/UUID_1_201_a.jpeg</span>
|
||||
@@ -453,98 +484,126 @@
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">_2_0_a.mov"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># don't know what it is!</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: unknown type </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s1">'type'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: unknown type </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s1">'type'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"renders"</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"renders"</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># TODO: might be possible for original/master to be missing but edit to still be there</span>
|
||||
<span class="c1"># if self._info["isMissing"] == 1:</span>
|
||||
<span class="c1"># photopath = None # path would be meaningless until downloaded</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited for Photos <= 4"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">></span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Wrong database format!"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"hasAdjustments"</span><span class="p">]:</span>
|
||||
<span class="n">edit_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"edit_resource_id"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">edit_id</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># todo: is this always true or do we need to search file file_id under folder_id</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="n">_PHOTO_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a photo</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.jpeg"</span>
|
||||
<span class="k">elif</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">==</span> <span class="n">_MOVIE_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a movie</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># don't know what it is!</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: unknown type </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s1">'type'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># photopath appears to usually be in "00" subfolder but</span>
|
||||
<span class="c1"># could be elsewhere--I haven't figured out this logic yet</span>
|
||||
<span class="c1"># first see if it's in 00</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="s2">"00"</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">rootdir</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
|
||||
<span class="c1"># check again to see if we found a valid file</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> hasAdjustments but edit_resource_id is None"</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_predicted_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return predicted path_edited for Photos <= 4"""</span>
|
||||
<span class="n">edit_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="k">if</span> <span class="n">uti_edited</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">uti_edited</span><span class="p">:</span>
|
||||
<span class="n">ext</span> <span class="o">=</span> <span class="n">get_preferred_uti_extension</span><span class="p">(</span><span class="n">uti_edited</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">ext</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.</span><span class="si">{</span><span class="n">ext</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># if we get here, we couldn't figure out the extension</span>
|
||||
<span class="c1"># so try to figure out the type and build the filename</span>
|
||||
<span class="n">type_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">type_</span> <span class="o">==</span> <span class="n">_PHOTO_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a photo</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.jpeg"</span>
|
||||
<span class="k">elif</span> <span class="n">type_</span> <span class="o">==</span> <span class="n">_MOVIE_TYPE</span><span class="p">:</span>
|
||||
<span class="c1"># it's a movie</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"fullsizeoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unknown type </span><span class="si">{</span><span class="n">type_</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return path_edited for Photos <= 4; modified version of code in PhotoInfo to debug #859"""</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"hasAdjustments"</span><span class="p">]:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">edit_id</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"edit_resource_id"</span><span class="p">]:</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_predicted_path_edited_4</span><span class="p">()</span>
|
||||
<span class="k">except</span> <span class="ne">ValueError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"ERROR: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># the heuristic failed, so try to find the file</span>
|
||||
<span class="n">rootdir</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">):</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">break</span>
|
||||
|
||||
<span class="c1"># check again to see if we found a valid file</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: edited file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> hasAdjustments but edit_resource_id is None"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">path_edited_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path to edited version of live photo movie; only valid for Photos 5+"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="sd">"""return path to edited version of live photo movie"""</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span>
|
||||
<span class="k">except</span> <span class="ne">AttributeError</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5_live_photo</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_4_live_photo</span><span class="p">()</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_5_live_photo</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_path_edited_live_photo</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_predicted_path_edited_live_photo_4</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="sd">"""return predicted path_edited for Photos <= 4"""</span>
|
||||
<span class="c1"># need the resource id for the video, not the photo (edit_resource_id is for photo)</span>
|
||||
<span class="k">if</span> <span class="n">edit_id</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"edit_resource_id_video"</span><span class="p">]:</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">edit_id</span><span class="p">)</span>
|
||||
<span class="c1"># figure out what kind it is and build filename</span>
|
||||
<span class="n">library</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"videocomplementoutput_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span>
|
||||
<span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library</span><span class="p">,</span> <span class="s2">"resources"</span><span class="p">,</span> <span class="s2">"media"</span><span class="p">,</span> <span class="s2">"version"</span><span class="p">,</span> <span class="n">folder_id</span><span class="p">,</span> <span class="n">nn_id</span><span class="p">,</span> <span class="n">filename</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_4_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited_live_photo for Photos <= 4"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">></span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="k">raise</span> <span class="ne">RuntimeError</span><span class="p">(</span><span class="s2">"Wrong database format!"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_predicted_path_edited_live_photo_4</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="ow">and</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="c1"># the heuristic failed, so try to find the file</span>
|
||||
<span class="n">rootdir</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span><span class="o">.</span><span class="n">parent</span>
|
||||
<span class="n">filename</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span><span class="o">.</span><span class="n">name</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">next</span><span class="p">(</span>
|
||||
<span class="p">(</span>
|
||||
<span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">dirname</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
|
||||
<span class="k">for</span> <span class="n">dirname</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">filelist</span> <span class="ow">in</span> <span class="n">os</span><span class="o">.</span><span class="n">walk</span><span class="p">(</span><span class="n">rootdir</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="ow">in</span> <span class="n">filelist</span>
|
||||
<span class="p">),</span>
|
||||
<span class="kc">None</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">photopath</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: edited live photo file for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">photopath</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_path_edited_5_live_photo</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return path_edited_live_photo for Photos >= 5"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><</span> <span class="n">_PHOTOS_5_VERSION</span><span class="p">:</span>
|
||||
@@ -604,15 +663,11 @@
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">filepath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"directory"</span><span class="p">])</span>
|
||||
|
||||
<span class="c1"># raw files have same name as original but with _4.raw_ext appended</span>
|
||||
<span class="c1"># I believe the _4 maps to PHAssetResourceTypeAlternatePhoto = 4</span>
|
||||
<span class="c1"># see: https://developer.apple.com/documentation/photokit/phassetresourcetype/phassetresourcetypealternatephoto?language=objc</span>
|
||||
<span class="n">raw_file</span> <span class="o">=</span> <span class="n">list_directory</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">startswith</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filestem</span><span class="si">}</span><span class="s2">_4"</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">raw_file</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">raw_file</span> <span class="o">:=</span> <span class="n">list_directory</span><span class="p">(</span><span class="n">filepath</span><span class="p">,</span> <span class="n">startswith</span><span class="o">=</span><span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">filestem</span><span class="si">}</span><span class="s2">_4"</span><span class="p">):</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">filepath</span><span class="p">)</span> <span class="o">/</span> <span class="n">raw_file</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">photopath</span><span class="p">)</span> <span class="k">if</span> <span class="n">photopath</span><span class="o">.</span><span class="n">is_file</span><span class="p">()</span> <span class="k">else</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># is a reference</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
@@ -640,7 +695,7 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_masters_path</span><span class="p">,</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"raw_info"</span><span class="p">][</span><span class="s2">"imagePath"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"MISSING PATH: RAW photo for UUID </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2"> should be at </span><span class="si">{</span><span class="n">photopath</span><span class="si">}</span><span class="s2"> but does not appear to exist"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -962,8 +1017,7 @@
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o"><</span> <span class="mi">7</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"UTI_raw"</span><span class="p">]</span>
|
||||
|
||||
<span class="n">rawpath</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span>
|
||||
<span class="k">if</span> <span class="n">rawpath</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">rawpath</span> <span class="o">:=</span> <span class="bp">self</span><span class="o">.</span><span class="n">path_raw</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">get_uti_for_extension</span><span class="p">(</span><span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">rawpath</span><span class="p">)</span><span class="o">.</span><span class="n">suffix</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
@@ -1057,10 +1111,10 @@
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">live_photo</span> <span class="ow">and</span> <span class="ow">not</span> <span class="bp">self</span><span class="o">.</span><span class="n">ismissing</span><span class="p">:</span>
|
||||
<span class="n">live_model_id</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"live_model_id"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">live_model_id</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"missing live_model_id: </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">live_model_id</span><span class="p">)</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">live_model_id</span><span class="p">)</span>
|
||||
<span class="n">library_path</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">library_path</span>
|
||||
<span class="n">photopath</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span>
|
||||
<span class="n">library_path</span><span class="p">,</span>
|
||||
@@ -1068,7 +1122,7 @@
|
||||
<span class="s2">"media"</span><span class="p">,</span>
|
||||
<span class="s2">"master"</span><span class="p">,</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span>
|
||||
<span class="s2">"00"</span><span class="p">,</span>
|
||||
<span class="n">nn_id</span><span class="p">,</span>
|
||||
<span class="sa">f</span><span class="s2">"jpegvideocomplement_</span><span class="si">{</span><span class="n">file_id</span><span class="si">}</span><span class="s2">.mov"</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">isfile</span><span class="p">(</span><span class="n">photopath</span><span class="p">):</span>
|
||||
@@ -1131,17 +1185,13 @@
|
||||
<span class="n">modelid</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"modelID"</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="n">modelid</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">modelid</span><span class="p">)</span>
|
||||
<span class="n">folder_id</span><span class="p">,</span> <span class="n">file_id</span><span class="p">,</span> <span class="n">nn_id</span> <span class="o">=</span> <span class="n">_get_resource_loc</span><span class="p">(</span><span class="n">modelid</span><span class="p">)</span>
|
||||
<span class="n">derivatives_root</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
|
||||
<span class="o">/</span> <span class="sa">f</span><span class="s2">"resources/proxies/derivatives/</span><span class="si">{</span><span class="n">folder_id</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># photos appears to usually be in "00" subfolder but</span>
|
||||
<span class="c1"># could be elsewhere--I haven't figured out this logic yet</span>
|
||||
<span class="c1"># first see if it's in 00</span>
|
||||
|
||||
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="s2">"00"</span> <span class="o">/</span> <span class="n">file_id</span>
|
||||
<span class="n">derivatives_path</span> <span class="o">=</span> <span class="n">derivatives_root</span> <span class="o">/</span> <span class="n">nn_id</span> <span class="o">/</span> <span class="n">file_id</span>
|
||||
<span class="k">if</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">is_dir</span><span class="p">():</span>
|
||||
<span class="n">files</span> <span class="o">=</span> <span class="n">derivatives_path</span><span class="o">.</span><span class="n">glob</span><span class="p">(</span><span class="s2">"*"</span><span class="p">)</span>
|
||||
<span class="n">files</span> <span class="o">=</span> <span class="nb">sorted</span><span class="p">(</span><span class="n">files</span><span class="p">,</span> <span class="n">reverse</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">key</span><span class="o">=</span><span class="k">lambda</span> <span class="n">f</span><span class="p">:</span> <span class="n">f</span><span class="o">.</span><span class="n">stat</span><span class="p">()</span><span class="o">.</span><span class="n">st_size</span><span class="p">)</span>
|
||||
@@ -1163,14 +1213,17 @@
|
||||
<span class="sd">"""Return paths to all derivative (preview) files for shared iCloud photos in Photos >= 5"""</span>
|
||||
<span class="n">directory</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_uuid</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="c1"># first char of uuid</span>
|
||||
<span class="c1"># only 1 derivative for shared photos and it's called 'UUID_4_5005_c.jpeg'</span>
|
||||
<span class="n">derivative_path</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">_PHOTOS_8_SHARED_DERIVATIVE_PATH</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">>=</span> <span class="mi">8</span>
|
||||
<span class="k">else</span> <span class="n">_PHOTOS_5_SHARED_DERIVATIVE_PATH</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">derivative_path</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_library_path</span><span class="p">)</span>
|
||||
<span class="o">/</span> <span class="s2">"resources/cloudsharing/resources/derivatives/masters"</span>
|
||||
<span class="o">/</span> <span class="n">derivative_path</span>
|
||||
<span class="o">/</span> <span class="sa">f</span><span class="s2">"</span><span class="si">{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/</span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">_4_5005_c.jpeg"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">derivative_path</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
|
||||
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">derivative_path</span><span class="p">)]</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="p">[</span><span class="nb">str</span><span class="p">(</span><span class="n">derivative_path</span><span class="p">)]</span> <span class="k">if</span> <span class="n">derivative_path</span><span class="o">.</span><span class="n">exists</span><span class="p">()</span> <span class="k">else</span> <span class="p">[]</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">panorama</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
@@ -1342,7 +1395,7 @@
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"score not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"score not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
@@ -1488,7 +1541,7 @@
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"exif_info not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"exif_info not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="kc">None</span>
|
||||
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
@@ -1515,7 +1568,7 @@
|
||||
<span class="n">lens_model</span><span class="o">=</span><span class="n">exif</span><span class="p">[</span><span class="s2">"ZLENSMODEL"</span><span class="p">],</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find exif record for uuid </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find exif record for uuid </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">exif_info</span> <span class="o">=</span> <span class="n">ExifInfo</span><span class="p">(</span>
|
||||
<span class="n">iso</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
<span class="n">flash_fired</span><span class="o">=</span><span class="kc">None</span><span class="p">,</span>
|
||||
@@ -1587,7 +1640,7 @@
|
||||
<span class="s2"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_db_version</span> <span class="o"><=</span> <span class="n">_PHOTOS_4_VERSION</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"cloud_metadata not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"cloud_metadata not implemented for this database version"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="p">{}</span>
|
||||
|
||||
<span class="n">_</span><span class="p">,</span> <span class="n">cursor</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
|
||||
@@ -1597,6 +1650,21 @@
|
||||
<span class="n">metadata</span> <span class="o">=</span> <span class="n">plistlib</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
|
||||
<span class="k">return</span> <span class="n">metadata</span>
|
||||
|
||||
<span class="nd">@cached_property</span>
|
||||
<span class="k">def</span> <span class="nf">cloud_guid</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="sd">"""Returns the GUID of the photo in iCloud (Photos 5+ only)"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"cloudMasterGUID"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@cached_property</span>
|
||||
<span class="k">def</span> <span class="nf">cloud_owner_hashed_id</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="sd">"""Returns the hashed iCloud ID of the owner of the shared photo (Photos 5+ only)"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"cloudownerhashedpersonid"</span><span class="p">]</span>
|
||||
|
||||
<span class="nd">@cached_property</span>
|
||||
<span class="k">def</span> <span class="nf">fingerprint</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
|
||||
<span class="sd">"""Returns fingerprint of original photo as a string"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_info</span><span class="p">[</span><span class="s2">"masterFingerprint"</span><span class="p">]</span>
|
||||
|
||||
<div class="viewcode-block" id="PhotoInfo.detected_text"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.detected_text">[docs]</a> <span class="k">def</span> <span class="nf">detected_text</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">confidence_threshold</span><span class="o">=</span><span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span><span class="p">):</span>
|
||||
<span class="sd">"""Detects text in photo and returns lists of results as (detected text, confidence)</span>
|
||||
|
||||
@@ -1982,6 +2050,9 @@
|
||||
<span class="s2">"comments"</span><span class="p">:</span> <span class="n">comments</span><span class="p">,</span>
|
||||
<span class="s2">"likes"</span><span class="p">:</span> <span class="n">likes</span><span class="p">,</span>
|
||||
<span class="s2">"search_info"</span><span class="p">:</span> <span class="n">search_info</span><span class="p">,</span>
|
||||
<span class="s2">"fingerprint"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">fingerprint</span><span class="p">,</span>
|
||||
<span class="s2">"cloud_guid"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_guid</span><span class="p">,</span>
|
||||
<span class="s2">"cloud_owner_hashed_id"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">cloud_owner_hashed_id</span><span class="p">,</span>
|
||||
<span class="p">}</span></div>
|
||||
|
||||
<div class="viewcode-block" id="PhotoInfo.json"><a class="viewcode-back" href="../../reference.html#osxphotos.PhotoInfo.json">[docs]</a> <span class="k">def</span> <span class="nf">json</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../../genindex.html" /><link rel="search" title="Search" href="../../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.photosdb.photosdb - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.photosdb.photosdb - osxphotos 0.57.2 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../../index.html"><div class="brand">osxphotos 0.57.2 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.2 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -160,7 +160,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../../reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../../reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -200,18 +200,21 @@
|
||||
<span class="sd">Processes a Photos.app library database to extract information about photos</span>
|
||||
<span class="sd">"""</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">__future__</span> <span class="kn">import</span> <span class="n">annotations</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">logging</span>
|
||||
<span class="kn">import</span> <span class="nn">os</span>
|
||||
<span class="kn">import</span> <span class="nn">os.path</span>
|
||||
<span class="kn">import</span> <span class="nn">pathlib</span>
|
||||
<span class="kn">import</span> <span class="nn">platform</span>
|
||||
<span class="kn">import</span> <span class="nn">re</span>
|
||||
<span class="kn">import</span> <span class="nn">sqlite3</span>
|
||||
<span class="kn">import</span> <span class="nn">sys</span>
|
||||
<span class="kn">import</span> <span class="nn">tempfile</span>
|
||||
<span class="kn">from</span> <span class="nn">collections</span> <span class="kn">import</span> <span class="n">OrderedDict</span>
|
||||
<span class="kn">from</span> <span class="nn">collections.abc</span> <span class="kn">import</span> <span class="n">Iterable</span>
|
||||
<span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span><span class="p">,</span> <span class="n">timedelta</span><span class="p">,</span> <span class="n">timezone</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Any</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span>
|
||||
<span class="kn">from</span> <span class="nn">unicodedata</span> <span class="kn">import</span> <span class="n">normalize</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">bitmath</span>
|
||||
@@ -247,7 +250,6 @@
|
||||
<span class="kn">from</span> <span class="nn">.._version</span> <span class="kn">import</span> <span class="n">__version__</span>
|
||||
<span class="kn">from</span> <span class="nn">..albuminfo</span> <span class="kn">import</span> <span class="n">AlbumInfo</span><span class="p">,</span> <span class="n">FolderInfo</span><span class="p">,</span> <span class="n">ImportInfo</span><span class="p">,</span> <span class="n">ProjectInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..datetime_utils</span> <span class="kn">import</span> <span class="n">datetime_has_tz</span><span class="p">,</span> <span class="n">datetime_naive_to_local</span>
|
||||
<span class="kn">from</span> <span class="nn">..debug</span> <span class="kn">import</span> <span class="n">is_debug</span>
|
||||
<span class="kn">from</span> <span class="nn">..fileutil</span> <span class="kn">import</span> <span class="n">FileUtil</span>
|
||||
<span class="kn">from</span> <span class="nn">..personinfo</span> <span class="kn">import</span> <span class="n">PersonInfo</span>
|
||||
<span class="kn">from</span> <span class="nn">..photoinfo</span> <span class="kn">import</span> <span class="n">PhotoInfo</span>
|
||||
@@ -257,13 +259,15 @@
|
||||
<span class="kn">from</span> <span class="nn">..sqlite_utils</span> <span class="kn">import</span> <span class="n">sqlite_db_is_locked</span><span class="p">,</span> <span class="n">sqlite_open_ro</span>
|
||||
<span class="kn">from</span> <span class="nn">..utils</span> <span class="kn">import</span> <span class="p">(</span>
|
||||
<span class="n">_check_file_exists</span><span class="p">,</span>
|
||||
<span class="n">_get_os_version</span><span class="p">,</span>
|
||||
<span class="n">get_macos_version</span><span class="p">,</span>
|
||||
<span class="n">get_last_library_path</span><span class="p">,</span>
|
||||
<span class="n">noop</span><span class="p">,</span>
|
||||
<span class="n">normalize_unicode</span><span class="p">,</span>
|
||||
<span class="p">)</span>
|
||||
<span class="kn">from</span> <span class="nn">.photosdb_utils</span> <span class="kn">import</span> <span class="n">get_db_model_version</span><span class="p">,</span> <span class="n">get_db_version</span>
|
||||
|
||||
<span class="n">logger</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">(</span><span class="s2">"osxphotos"</span><span class="p">)</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"PhotosDB"</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># TODO: Add test for imageTimeZoneOffsetSeconds = None</span>
|
||||
@@ -311,7 +315,7 @@
|
||||
|
||||
<span class="c1"># Check OS version</span>
|
||||
<span class="n">system</span> <span class="o">=</span> <span class="n">platform</span><span class="o">.</span><span class="n">system</span><span class="p">()</span>
|
||||
<span class="p">(</span><span class="n">ver</span><span class="p">,</span> <span class="n">major</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">_get_os_version</span><span class="p">()</span>
|
||||
<span class="p">(</span><span class="n">ver</span><span class="p">,</span> <span class="n">major</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">get_macos_version</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="n">system</span> <span class="o">!=</span> <span class="s2">"Darwin"</span> <span class="ow">or</span> <span class="p">((</span><span class="n">ver</span><span class="p">,</span> <span class="n">major</span><span class="p">)</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">_TESTED_OS_VERSIONS</span><span class="p">):</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: This module has only been tested with macOS versions "</span>
|
||||
@@ -477,8 +481,7 @@
|
||||
<span class="c1"># key is Z_PK of ZMOMENT table and values are the moment info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_moment_pk</span> <span class="o">=</span> <span class="p">{}</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">dbfile</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
|
||||
<span class="n">dbfile</span> <span class="o">=</span> <span class="n">get_last_library_path</span><span class="p">()</span>
|
||||
@@ -494,8 +497,7 @@
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">_check_file_exists</span><span class="p">(</span><span class="n">dbfile</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2"> does not exist"</span><span class="p">,</span> <span class="n">dbfile</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"dbfile = </span><span class="si">{</span><span class="n">dbfile</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># init database names</span>
|
||||
<span class="c1"># _tmp_db is the file that will processed by _process_database4/5</span>
|
||||
@@ -521,8 +523,15 @@
|
||||
<span class="c1"># _db_version is set from photos.db</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">=</span> <span class="n">get_db_version</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
|
||||
<span class="c1"># _photos_version is set from Photos.sqlite which only exists for Photos 5+</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">4</span> <span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span> <span class="o">==</span> <span class="mi">4</span> <span class="k">else</span> <span class="mi">5</span>
|
||||
|
||||
<span class="n">db_ver_int</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">db_ver_int</span> <span class="o"><</span> <span class="mi">3000</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">2</span>
|
||||
<span class="k">elif</span> <span class="n">db_ver_int</span> <span class="o"><</span> <span class="mi">4000</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">3</span>
|
||||
<span class="k">elif</span> <span class="n">db_ver_int</span> <span class="o"><</span> <span class="mi">5000</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">4</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="mi">5</span>
|
||||
<span class="c1"># If Photos >= 5, actual data isn't in photos.db but in Photos.sqlite</span>
|
||||
<span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span> <span class="o">></span> <span class="nb">int</span><span class="p">(</span><span class="n">_PHOTOS_4_VERSION</span><span class="p">):</span>
|
||||
<span class="n">dbpath</span> <span class="o">=</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="p">)</span><span class="o">.</span><span class="n">parent</span>
|
||||
@@ -539,10 +548,9 @@
|
||||
<span class="c1"># set the photos version to actual value based on Photos.sqlite</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o">=</span> <span class="n">get_db_model_version</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"_dbfile = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="si">}</span><span class="s2">, _dbfile_actual = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"_dbfile = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile</span><span class="si">}</span><span class="s2">, _dbfile_actual = </span><span class="si">{</span><span class="bp">self</span><span class="o">.</span><span class="n">_dbfile_actual</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="n">library_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">abspath</span><span class="p">(</span><span class="n">dbfile</span><span class="p">))</span>
|
||||
<span class="p">(</span><span class="n">library_path</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">library_path</span><span class="p">)</span> <span class="c1"># drop /database from path</span>
|
||||
@@ -554,8 +562,7 @@
|
||||
<span class="n">masters_path</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">library_path</span><span class="p">,</span> <span class="s2">"originals"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_masters_path</span> <span class="o">=</span> <span class="n">masters_path</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"library = </span><span class="si">{</span><span class="n">library_path</span><span class="si">}</span><span class="s2">, masters = </span><span class="si">{</span><span class="n">masters_path</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"library = </span><span class="si">{</span><span class="n">library_path</span><span class="si">}</span><span class="s2">, masters = </span><span class="si">{</span><span class="n">masters_path</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="nb">int</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_db_version</span><span class="p">)</span> <span class="o"><=</span> <span class="nb">int</span><span class="p">(</span><span class="n">_PHOTOS_4_VERSION</span><span class="p">):</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_process_database4</span><span class="p">()</span>
|
||||
@@ -782,6 +789,11 @@
|
||||
<span class="sd">"""returns path to the Photos library PhotosDB was initialized with"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_library_path</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">photos_version</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns version of Photos app that created the library"""</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photos_ver</span>
|
||||
|
||||
<div class="viewcode-block" id="PhotosDB.get_db_connection"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.get_db_connection">[docs]</a> <span class="k">def</span> <span class="nf">get_db_connection</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Get connection to the working copy of the Photos database</span>
|
||||
|
||||
@@ -810,38 +822,10 @@
|
||||
<span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Error copying</span><span class="si">{</span><span class="n">fname</span><span class="si">}</span><span class="s2"> to </span><span class="si">{</span><span class="n">dest_path</span><span class="si">}</span><span class="s2">"</span><span class="p">,</span> <span class="n">file</span><span class="o">=</span><span class="n">sys</span><span class="o">.</span><span class="n">stderr</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="ne">Exception</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">dest_path</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="n">dest_path</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">dest_path</span>
|
||||
|
||||
<span class="c1"># NOTE: This method seems to cause problems with applescript</span>
|
||||
<span class="c1"># Bummer...would'be been nice to avoid copying the DB</span>
|
||||
<span class="c1"># def _link_db_file(self, fname):</span>
|
||||
<span class="c1"># """ links the sqlite database file to a temp file """</span>
|
||||
<span class="c1"># """ returns the name of the temp file """</span>
|
||||
<span class="c1"># """ If sqlite shared memory and write-ahead log files exist, those are copied too """</span>
|
||||
<span class="c1"># # required because python's sqlite3 implementation can't read a locked file</span>
|
||||
<span class="c1"># # _, suffix = os.path.splitext(fname)</span>
|
||||
<span class="c1"># dest_name = dest_path = ""</span>
|
||||
<span class="c1"># try:</span>
|
||||
<span class="c1"># dest_name = pathlib.Path(fname).name</span>
|
||||
<span class="c1"># dest_path = os.path.join(self._tempdir_name, dest_name)</span>
|
||||
<span class="c1"># FileUtil.hardlink(fname, dest_path)</span>
|
||||
<span class="c1"># # link write-ahead log and shared memory files (-wal and -shm) files if they exist</span>
|
||||
<span class="c1"># if os.path.exists(f"{fname}-wal"):</span>
|
||||
<span class="c1"># FileUtil.hardlink(f"{fname}-wal", f"{dest_path}-wal")</span>
|
||||
<span class="c1"># if os.path.exists(f"{fname}-shm"):</span>
|
||||
<span class="c1"># FileUtil.hardlink(f"{fname}-shm", f"{dest_path}-shm")</span>
|
||||
<span class="c1"># except:</span>
|
||||
<span class="c1"># print("Error linking " + fname + " to " + dest_path, file=sys.stderr)</span>
|
||||
<span class="c1"># raise Exception</span>
|
||||
|
||||
<span class="c1"># if is_debug():</span>
|
||||
<span class="c1"># logging.debug(dest_path)</span>
|
||||
|
||||
<span class="c1"># return dest_path</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_process_database4</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""process the Photos database to extract info</span>
|
||||
<span class="sd"> works on Photos version <= 4.0"""</span>
|
||||
@@ -890,6 +874,8 @@
|
||||
<span class="s2">"displayname"</span><span class="p">:</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">4</span><span class="p">]),</span>
|
||||
<span class="s2">"photo_uuid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
|
||||
<span class="s2">"keyface_uuid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
|
||||
<span class="s2">"type"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="s2">"manualorder"</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="p">}</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</span><span class="p">[</span><span class="n">fullname</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pk</span><span class="p">)</span>
|
||||
@@ -921,7 +907,7 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">][</span><span class="s2">"photo_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">][</span><span class="s2">"keyface_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unexpected KeyError _dbpersons_pk[</span><span class="si">{</span><span class="n">pk</span><span class="si">}</span><span class="s2">]"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unexpected KeyError _dbpersons_pk[</span><span class="si">{</span><span class="n">pk</span><span class="si">}</span><span class="s2">]"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># get information on detected faces</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing detected faces in photos."</span><span class="p">)</span>
|
||||
@@ -1019,20 +1005,20 @@
|
||||
<span class="s2">"cloudlibrarystate"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">2</span><span class="p">],</span>
|
||||
<span class="s2">"cloudidentifier"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">3</span><span class="p">],</span>
|
||||
<span class="s2">"intrash"</span><span class="p">:</span> <span class="kc">False</span> <span class="k">if</span> <span class="n">album</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">True</span><span class="p">,</span>
|
||||
<span class="s2">"cloudlocalstate"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5</span>
|
||||
<span class="s2">"cloudownerfirstname"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5</span>
|
||||
<span class="s2">"cloudownderlastname"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5</span>
|
||||
<span class="s2">"cloudownerhashedpersonid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5</span>
|
||||
<span class="s2">"cloudlocalstate"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="s2">"cloudownerfirstname"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="s2">"cloudownderlastname"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="s2">"cloudownerhashedpersonid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="s2">"folderUuid"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">5</span><span class="p">],</span>
|
||||
<span class="s2">"albumType"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">6</span><span class="p">],</span>
|
||||
<span class="s2">"albumSubclass"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="c1"># for compatability with Photos 5 where album kind is ZKIND</span>
|
||||
<span class="s2">"kind"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"customsortkey"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5 only</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="s2">"customsortkey"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span> <span class="c1"># Photos 5+ only</span>
|
||||
<span class="p">}</span>
|
||||
|
||||
<span class="c1"># get details about folders</span>
|
||||
@@ -1150,7 +1136,8 @@
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid,</span>
|
||||
<span class="sd"> RKMaster.fingerprint</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1182,7 +1169,8 @@
|
||||
<span class="sd"> RKVersion.inTrashDate,</span>
|
||||
<span class="sd"> RKVersion.showInLibrary,</span>
|
||||
<span class="sd"> RKMaster.fileIsReference,</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid</span>
|
||||
<span class="sd"> RKMaster.importGroupUuid,</span>
|
||||
<span class="sd"> RKMaster.fingerprint</span>
|
||||
<span class="sd"> FROM RKVersion, RKMaster</span>
|
||||
<span class="sd"> WHERE RKVersion.masterUuid = RKMaster.uuid"""</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1233,6 +1221,7 @@
|
||||
<span class="c1"># 42 RKVersion.showInLibrary -- is item visible in library (e.g. non-selected burst images are not visible)</span>
|
||||
<span class="c1"># 43 RKMaster.fileIsReference -- file is reference (imported without copying to Photos library)</span>
|
||||
<span class="c1"># 44 RKMaster.importGroupUuid -- to get date added from RKImportGroup</span>
|
||||
<span class="c1"># 45 RKMaster.fingerprint -- fingerprint / hash of the file</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1295,8 +1284,7 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># unknown</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">21</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">22</span><span class="p">]</span>
|
||||
@@ -1361,12 +1349,13 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"momentID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">28</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># Init cloud details that will be filled in later if cloud asset</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudAssetGUID"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudLocalState"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudAssetGUID"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudLocalState"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5+</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudLibraryState"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudStatus"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudAvailable"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"incloud"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudMasterGUID"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 5+</span>
|
||||
|
||||
<span class="c1"># associated RAW image info</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"has_raw"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span> <span class="o">==</span> <span class="mi">7</span> <span class="k">else</span> <span class="kc">False</span>
|
||||
@@ -1430,6 +1419,9 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">44</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"fok_import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># fingerprint</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"masterFingerprint"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">45</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># photos 5+ only, for shared photos</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudownerhashedpersonid"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
@@ -1494,7 +1486,9 @@
|
||||
<span class="sd"> RKModelResource.resourceTag, RKModelResource.UTI, RKVersion.specialType,</span>
|
||||
<span class="sd"> RKModelResource.attachedModelType, RKModelResource.resourceType</span>
|
||||
<span class="sd"> FROM RKVersion</span>
|
||||
<span class="sd"> JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId """</span>
|
||||
<span class="sd"> JOIN RKModelResource on RKModelResource.attachedModelId = RKVersion.modelId</span>
|
||||
<span class="sd"> ORDER BY RKModelResource.modelId</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># Order of results:</span>
|
||||
@@ -1504,8 +1498,8 @@
|
||||
<span class="c1"># 3 RKModelResource.resourceTag</span>
|
||||
<span class="c1"># 4 RKModelResource.UTI</span>
|
||||
<span class="c1"># 5 RKVersion.specialType</span>
|
||||
<span class="c1"># 6 RKModelResource.attachedModelType</span>
|
||||
<span class="c1"># 7 RKModelResource.resourceType</span>
|
||||
<span class="c1"># 6 RKModelResource.attachedModelType (2 = edit)</span>
|
||||
<span class="c1"># 7 RKModelResource.resourceType (4 = photo, 8 = video)</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">row</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1517,18 +1511,28 @@
|
||||
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">"UNADJUSTED"</span>
|
||||
<span class="ow">and</span> <span class="n">row</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">==</span> <span class="mi">2</span>
|
||||
<span class="p">):</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found more than one edit_resource_id for "</span>
|
||||
<span class="n">resource_type</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">7</span><span class="p">]</span>
|
||||
<span class="c1"># UTI_edited will be set to the appropriate UTI for the edited resource below</span>
|
||||
<span class="c1"># a live photo that's edited will have both a photo and video resource but the photo</span>
|
||||
<span class="c1"># UTI will be used for the edited live photo, see #859</span>
|
||||
<span class="k">if</span> <span class="n">resource_type</span> <span class="o">==</span> <span class="mi">4</span><span class="p">:</span>
|
||||
<span class="c1"># photo</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_photo"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found more than one edit_resource_id_photo for "</span>
|
||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="c1"># TODO: I think there should never be more than one edit but</span>
|
||||
<span class="c1"># I've seen this once in my library</span>
|
||||
<span class="c1"># should we return all edits or just most recent one?</span>
|
||||
<span class="c1"># For now, return most recent edit</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">resource_type</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="c1"># video</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_video"</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found more than one edit_resource_id_video for "</span>
|
||||
<span class="sa">f</span><span class="s2">"UUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">,adjustmentUUID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2">, modelID </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">4</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># get details on external edits</span>
|
||||
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
@@ -1581,9 +1585,27 @@
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># init any uuids that had no edits or live photos</span>
|
||||
<span class="c1"># also initialized UTI_edited and edit_resource_id</span>
|
||||
<span class="k">for</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_photo"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"edit_resource_id_video"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"UTI_edited_photo"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="k">if</span> <span class="s2">"UTI_edited_video"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="c1"># UTI_edited will be set to the appropriate UTI for the edited resource below</span>
|
||||
<span class="c1"># a live photo that's edited will have both a photo and video resource but the photo</span>
|
||||
<span class="c1"># UTI will be used for the edited live photo</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id"</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="s2">"live_model_id"</span> <span class="ow">not</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"live_model_id"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"modeResourceIsOnDisk"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -1797,8 +1819,7 @@
|
||||
<span class="sd"> but it works so don't touch it.</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"_process_database5"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"_process_database5"</span><span class="p">)</span>
|
||||
<span class="n">verbose</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_verbose</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Processing database."</span><span class="p">)</span>
|
||||
<span class="p">(</span><span class="n">conn</span><span class="p">,</span> <span class="n">c</span><span class="p">)</span> <span class="o">=</span> <span class="n">sqlite_open_ro</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_tmp_db</span><span class="p">)</span>
|
||||
@@ -1821,8 +1842,7 @@
|
||||
<span class="n">hdr_type_column</span> <span class="o">=</span> <span class="n">_DB_TABLE_NAMES</span><span class="p">[</span><span class="n">photos_ver</span><span class="p">][</span><span class="s2">"HDR_TYPE"</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># Look for all combinations of persons and pictures</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Getting information about persons"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Getting information about persons"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># get info to associate persons with photos</span>
|
||||
<span class="c1"># then get detected faces in each photo and link to persons</span>
|
||||
@@ -1834,7 +1854,9 @@
|
||||
<span class="sd"> ZPERSON.ZFULLNAME,</span>
|
||||
<span class="sd"> ZPERSON.ZFACECOUNT,</span>
|
||||
<span class="sd"> ZPERSON.ZKEYFACE,</span>
|
||||
<span class="sd"> ZPERSON.ZDISPLAYNAME</span>
|
||||
<span class="sd"> ZPERSON.ZDISPLAYNAME,</span>
|
||||
<span class="sd"> ZPERSON.ZTYPE,</span>
|
||||
<span class="sd"> ZPERSON.ZMANUALORDER</span>
|
||||
<span class="sd"> FROM ZPERSON</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="p">)</span>
|
||||
@@ -1845,6 +1867,8 @@
|
||||
<span class="c1"># 3 ZPERSON.ZFACECOUNT,</span>
|
||||
<span class="c1"># 4 ZPERSON.ZKEYFACE,</span>
|
||||
<span class="c1"># 5 ZPERSON.ZDISPLAYNAME</span>
|
||||
<span class="c1"># 6 ZPERSON.ZTYPE, # ZTYPE = 1 == favorite, 0 == not favorite</span>
|
||||
<span class="c1"># 7 ZPERSON.ZMANUALORDER # favorites are sorted by ZMANUALORDER</span>
|
||||
|
||||
<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">c</span><span class="p">:</span>
|
||||
<span class="n">pk</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span>
|
||||
@@ -1862,6 +1886,8 @@
|
||||
<span class="s2">"displayname"</span><span class="p">:</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">person</span><span class="p">[</span><span class="mi">5</span><span class="p">]),</span>
|
||||
<span class="s2">"photo_uuid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
|
||||
<span class="s2">"keyface_uuid"</span><span class="p">:</span> <span class="kc">None</span><span class="p">,</span>
|
||||
<span class="s2">"type"</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">6</span><span class="p">],</span>
|
||||
<span class="s2">"manualorder"</span><span class="p">:</span> <span class="n">person</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="p">}</span>
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_fullname</span><span class="p">[</span><span class="n">fullname</span><span class="p">]</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">pk</span><span class="p">)</span>
|
||||
@@ -1893,7 +1919,7 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">][</span><span class="s2">"photo_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbpersons_pk</span><span class="p">[</span><span class="n">pk</span><span class="p">][</span><span class="s2">"keyface_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">person</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unexpected KeyError _dbpersons_pk[</span><span class="si">{</span><span class="n">pk</span><span class="si">}</span><span class="s2">]"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unexpected KeyError _dbpersons_pk[</span><span class="si">{</span><span class="n">pk</span><span class="si">}</span><span class="s2">]"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># get information on detected faces</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing detected faces in photos."</span><span class="p">)</span>
|
||||
@@ -1989,7 +2015,8 @@
|
||||
<span class="s2">"parentfolder"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">7</span><span class="p">],</span>
|
||||
<span class="s2">"pk"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">8</span><span class="p">],</span>
|
||||
<span class="s2">"intrash"</span><span class="p">:</span> <span class="kc">False</span> <span class="k">if</span> <span class="n">album</span><span class="p">[</span><span class="mi">9</span><span class="p">]</span> <span class="o">==</span> <span class="mi">0</span> <span class="k">else</span> <span class="kc">True</span><span class="p">,</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span> <span class="c1"># iPhone Photos.sqlite can have null value</span>
|
||||
<span class="s2">"creation_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">10</span><span class="p">]</span>
|
||||
<span class="ow">or</span> <span class="mi">0</span><span class="p">,</span> <span class="c1"># iPhone Photos.sqlite can have null value</span>
|
||||
<span class="s2">"start_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">11</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="s2">"end_date"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">12</span><span class="p">]</span> <span class="ow">or</span> <span class="mi">0</span><span class="p">,</span>
|
||||
<span class="s2">"customsortascending"</span><span class="p">:</span> <span class="n">album</span><span class="p">[</span><span class="mi">13</span><span class="p">],</span>
|
||||
@@ -2230,8 +2257,7 @@
|
||||
<span class="k">elif</span> <span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="n">_MOVIE_TYPE</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"WARNING: </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> found unknown type </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">17</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"type"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"UTI"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">18</span><span class="p">]</span>
|
||||
@@ -2292,6 +2318,7 @@
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"cloudLibraryState"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"cloudStatus"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"cloudAvailable"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"cloudMasterGUID"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="c1"># reverse geolocation info</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"reverse_geolocation"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">25</span><span class="p">]</span>
|
||||
@@ -2366,6 +2393,12 @@
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"alt_master_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"raw_info"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span> <span class="c1"># Photos 4</span>
|
||||
|
||||
<span class="c1"># Photos 4 only</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"edit_resource_id_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"edit_resource_id_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"UTI_edited_photo"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">info</span><span class="p">[</span><span class="s2">"UTI_edited_video"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">]</span> <span class="o">=</span> <span class="n">info</span>
|
||||
|
||||
<span class="c1"># compute signatures for finding possible duplicates</span>
|
||||
@@ -2412,7 +2445,7 @@
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"fok_import_session"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"import_uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"No info record for uuid </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> for import session"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"No info record for uuid </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2"> for import session"</span><span class="p">)</span>
|
||||
|
||||
<span class="c1"># Get extended description</span>
|
||||
<span class="n">verbose</span><span class="p">(</span><span class="s2">"Processing additional photo details."</span><span class="p">)</span>
|
||||
@@ -2429,10 +2462,9 @@
|
||||
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"extendedDescription"</span><span class="p">]</span> <span class="o">=</span> <span class="n">normalize_unicode</span><span class="p">(</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found description </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found description </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for </span><span class="si">{</span><span class="n">uuid</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># get information about adjusted/edited photos</span>
|
||||
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
@@ -2448,10 +2480,9 @@
|
||||
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"adjustmentFormatID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">is_debug</span><span class="p">():</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found adjustmentformatidentifier </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for uuid </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"WARNING: found adjustmentformatidentifier </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span><span class="si">}</span><span class="s2"> but no photo for uuid </span><span class="si">{</span><span class="n">row</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># Find missing photos</span>
|
||||
<span class="c1"># TODO: this code is very kludgy and I had to make lots of assumptions</span>
|
||||
@@ -2534,7 +2565,8 @@
|
||||
<span class="n">c</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">""" SELECT</span>
|
||||
<span class="s2"> </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZUUID,</span>
|
||||
<span class="s2"> ZCLOUDMASTER.ZCLOUDLOCALSTATE</span>
|
||||
<span class="s2"> ZCLOUDMASTER.ZCLOUDLOCALSTATE,</span>
|
||||
<span class="s2"> ZCLOUDMASTER.ZCLOUDMASTERGUID</span>
|
||||
<span class="s2"> FROM ZCLOUDMASTER, </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2"></span>
|
||||
<span class="s2"> WHERE </span><span class="si">{</span><span class="n">asset_table</span><span class="si">}</span><span class="s2">.ZMASTER = ZCLOUDMASTER.Z_PK """</span>
|
||||
<span class="p">)</span>
|
||||
@@ -2543,6 +2575,7 @@
|
||||
<span class="k">if</span> <span class="n">uuid</span> <span class="ow">in</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">:</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudLocalState"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"incloud"</span><span class="p">]</span> <span class="o">=</span> <span class="kc">True</span> <span class="k">if</span> <span class="n">row</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="mi">3</span> <span class="k">else</span> <span class="kc">False</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_dbphotos</span><span class="p">[</span><span class="n">uuid</span><span class="p">][</span><span class="s2">"cloudMasterGUID"</span><span class="p">]</span> <span class="o">=</span> <span class="n">row</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>
|
||||
|
||||
<span class="c1"># get information about associted RAW images</span>
|
||||
<span class="c1"># RAW images have ZDATASTORESUBTYPE = 17</span>
|
||||
@@ -2825,7 +2858,7 @@
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Caught _dbalbum_folders KeyError for album: </span><span class="si">{</span><span class="n">album_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Caught _dbalbum_folders KeyError for album: </span><span class="si">{</span><span class="n">album_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
|
||||
@@ -2862,7 +2895,7 @@
|
||||
<span class="k">try</span><span class="p">:</span>
|
||||
<span class="n">folders</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_dbalbum_folders</span><span class="p">[</span><span class="n">album_uuid</span><span class="p">]</span>
|
||||
<span class="k">except</span> <span class="ne">KeyError</span><span class="p">:</span>
|
||||
<span class="n">logging</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Caught _dbalbum_folders KeyError for album: </span><span class="si">{</span><span class="n">album_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Caught _dbalbum_folders KeyError for album: </span><span class="si">{</span><span class="n">album_uuid</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_recurse_folder_hierarchy</span><span class="p">(</span><span class="n">folders</span><span class="p">,</span> <span class="n">hierarchy</span><span class="o">=</span><span class="p">[]):</span>
|
||||
@@ -3393,6 +3426,8 @@
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">edited</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">]</span>
|
||||
<span class="k">elif</span> <span class="n">options</span><span class="o">.</span><span class="n">not_edited</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="ow">not</span> <span class="n">p</span><span class="o">.</span><span class="n">hasadjustments</span><span class="p">]</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">external_edit</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span><span class="n">p</span> <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">photos</span> <span class="k">if</span> <span class="n">p</span><span class="o">.</span><span class="n">external_edit</span><span class="p">]</span>
|
||||
@@ -3549,7 +3584,7 @@
|
||||
<span class="k">if</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">filename</span> <span class="ow">or</span> <span class="n">n</span> <span class="ow">in</span> <span class="n">p</span><span class="o">.</span><span class="n">original_filename</span>
|
||||
<span class="p">]</span>
|
||||
<span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">photo_list</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">photo_list</span><span class="p">))</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">min_size</span><span class="p">:</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="p">[</span>
|
||||
@@ -3656,7 +3691,7 @@
|
||||
<span class="n">exifdata_value</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="n">exifdata_value</span><span class="p">)</span>
|
||||
<span class="k">if</span> <span class="n">exifvalue</span> <span class="ow">in</span> <span class="n">exifdata_value</span><span class="p">:</span>
|
||||
<span class="n">matching_photos</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">p</span><span class="p">)</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="n">matching_photos</span>
|
||||
<span class="n">photos</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="nb">set</span><span class="p">(</span><span class="n">matching_photos</span><span class="p">))</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">options</span><span class="o">.</span><span class="n">added_after</span><span class="p">:</span>
|
||||
<span class="n">added_after</span> <span class="o">=</span> <span class="n">options</span><span class="o">.</span><span class="n">added_after</span>
|
||||
@@ -3703,10 +3738,11 @@
|
||||
|
||||
<span class="k">return</span> <span class="n">photos</span></div>
|
||||
|
||||
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">):</span>
|
||||
<div class="viewcode-block" id="PhotosDB.execute"><a class="viewcode-back" href="../../../reference.html#osxphotos.PhotosDB.execute">[docs]</a> <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">sql</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">params</span><span class="p">:</span> <span class="n">Any</span> <span class="o">|</span> <span class="kc">None</span> <span class="o">=</span> <span class="kc">None</span><span class="p">)</span> <span class="o">-></span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">Cursor</span><span class="p">:</span>
|
||||
<span class="sd">"""Execute sql statement and return cursor"""</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">get_db_connection</span><span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">)</span></div>
|
||||
<span class="n">params</span> <span class="o">=</span> <span class="n">params</span> <span class="ow">or</span> <span class="p">()</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_db_connection</span><span class="o">.</span><span class="n">cursor</span><span class="p">()</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="n">sql</span><span class="p">,</span> <span class="n">params</span><span class="p">)</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_duplicate_signature</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">uuid</span><span class="p">):</span>
|
||||
<span class="sd">"""Compute a signature for finding possible duplicates"""</span>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.phototemplate - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.phototemplate - osxphotos 0.57.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.57.1 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.1 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -211,6 +211,8 @@
|
||||
|
||||
<span class="kn">from</span> <span class="nn">textx</span> <span class="kn">import</span> <span class="n">TextXSyntaxError</span><span class="p">,</span> <span class="n">metamodel_from_file</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">osxphotos.template_counter</span> <span class="k">as</span> <span class="nn">counter</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="n">_UNKNOWN_PERSON</span><span class="p">,</span> <span class="n">TEXT_DETECTION_CONFIDENCE_THRESHOLD</span>
|
||||
<span class="kn">from</span> <span class="nn">._version</span> <span class="kn">import</span> <span class="n">__version__</span>
|
||||
<span class="kn">from</span> <span class="nn">.datetime_formatter</span> <span class="kn">import</span> <span class="n">DateTimeFormatter</span>
|
||||
@@ -348,6 +350,24 @@
|
||||
<span class="o">+</span> <span class="s2">"May be formatted using a python string format code. "</span>
|
||||
<span class="o">+</span> <span class="s2">"For example, to format as a 5-digit integer and pad with zeros, use '</span><span class="si">{id:05d}</span><span class="s2">' which results in "</span>
|
||||
<span class="o">+</span> <span class="s2">"00001, 00002, 00003...etc. "</span><span class="p">,</span>
|
||||
<span class="s2">"</span><span class="si">{counter}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"A sequential counter, starting at 0, that increments each time it is evaluated."</span>
|
||||
<span class="o">+</span> <span class="s2">"To start counting at a value other than 0, append append '(starting_value)' to the field name."</span>
|
||||
<span class="o">+</span> <span class="s2">"For example, to start counting at 1 instead of 0: '{counter(1)}'."</span>
|
||||
<span class="o">+</span> <span class="s2">"May be formatted using a python string format code."</span>
|
||||
<span class="o">+</span> <span class="s2">"For example, to format as a 5-digit integer and pad with zeros, use '{counter:05d(1)}'"</span>
|
||||
<span class="o">+</span> <span class="s2">"which results in 00001, 00002, 00003...etc."</span>
|
||||
<span class="o">+</span> <span class="s2">"You may also specify a stop value which causes the counter to reset to the starting value"</span>
|
||||
<span class="o">+</span> <span class="s2">"when the stop value is reached and a step size which causes the counter to increment by"</span>
|
||||
<span class="o">+</span> <span class="s2">"the specified value instead of 1. Use the format '{counter(start,stop,step)}' where start,"</span>
|
||||
<span class="o">+</span> <span class="s2">"stop, and step are integers. For example, to count from 1 to 10 by 2, use '{counter(1,11,2)}'."</span>
|
||||
<span class="o">+</span> <span class="s2">"Note that the counter stops counting when the stop value is reached and does not return the"</span>
|
||||
<span class="o">+</span> <span class="s2">"stop value. Start, stop, and step are optional and may be omitted. For example, to count"</span>
|
||||
<span class="o">+</span> <span class="s2">"from 0 by 2s, use '{counter(,,2)}'."</span>
|
||||
<span class="o">+</span> <span class="s2">"You may create an arbitrary number of counters by appending a unique name to the field name"</span>
|
||||
<span class="o">+</span> <span class="s2">"preceded by a period: '</span><span class="si">{counter.a}</span><span class="s2">', '</span><span class="si">{counter.b}</span><span class="s2">', etc. Each counter will have its own state"</span>
|
||||
<span class="o">+</span> <span class="s2">"and will start at 0 and increment by 1 unless otherwise specified."</span>
|
||||
<span class="o">+</span> <span class="s2">" Note: </span><span class="si">{counter}</span><span class="s2"> is not suitable for use with 'export' and '--update' "</span>
|
||||
<span class="o">+</span> <span class="s2">"as the counter associated with a photo may change between export sessions. See also </span><span class="si">{id}</span><span class="s2">."</span><span class="p">,</span>
|
||||
<span class="s2">"</span><span class="si">{album_seq}</span><span class="s2">"</span><span class="p">:</span> <span class="s2">"An integer, starting at 0, indicating the photo's index (sequence) in the containing album. "</span>
|
||||
<span class="o">+</span> <span class="s2">"Only valid when used in a '--filename' template and only when '</span><span class="si">{album}</span><span class="s2">' or '</span><span class="si">{folder_album}</span><span class="s2">' is used in the '--directory' template. "</span>
|
||||
<span class="o">+</span> <span class="s1">'For example </span><span class="se">\'</span><span class="s1">--directory "</span><span class="si">{folder_album}</span><span class="s1">" --filename "</span><span class="si">{album_seq}</span><span class="s1">_</span><span class="si">{original_name}</span><span class="s1">"</span><span class="se">\'</span><span class="s1">. '</span>
|
||||
@@ -498,6 +518,9 @@
|
||||
<span class="n">INPLACE_DEFAULT</span> <span class="o">=</span> <span class="s2">","</span>
|
||||
<span class="n">PATH_SEP_DEFAULT</span> <span class="o">=</span> <span class="n">os</span><span class="o">.</span><span class="n">path</span><span class="o">.</span><span class="n">sep</span>
|
||||
|
||||
<span class="c1"># globals for tracking {seq} substitutions</span>
|
||||
<span class="n">_global_seq_count</span> <span class="o">=</span> <span class="mi">0</span>
|
||||
|
||||
<span class="n">PUNCTUATION</span> <span class="o">=</span> <span class="p">{</span>
|
||||
<span class="s2">"comma"</span><span class="p">:</span> <span class="s2">","</span><span class="p">,</span>
|
||||
<span class="s2">"semicolon"</span><span class="p">:</span> <span class="s2">";"</span><span class="p">,</span>
|
||||
@@ -1116,6 +1139,8 @@
|
||||
<span class="n">start_id</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">field_arg</span><span class="p">)</span> <span class="k">if</span> <span class="n">field_arg</span> <span class="ow">is</span> <span class="ow">not</span> <span class="kc">None</span> <span class="k">else</span> <span class="mi">0</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">+</span> <span class="n">start_id</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">format_str_value</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">subfield</span><span class="p">)</span>
|
||||
<span class="k">elif</span> <span class="n">field</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">"counter"</span><span class="p">):</span>
|
||||
<span class="n">value</span> <span class="o">=</span> <span class="n">counter</span><span class="o">.</span><span class="n">get_counter_value</span><span class="p">(</span><span class="n">field</span><span class="p">,</span> <span class="n">subfield</span><span class="p">,</span> <span class="n">field_arg</span><span class="p">)</span>
|
||||
<span class="k">else</span><span class="p">:</span>
|
||||
<span class="c1"># if here, didn't get a match</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Unhandled template value: </span><span class="si">{</span><span class="n">field</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<!doctype html>
|
||||
<html class="no-js">
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-4.4.0, furo 2022.04.07"/>
|
||||
<title>osxphotos.queryoptions - osxphotos 0.50.13 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.queryoptions - osxphotos 0.57.1 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=68f4518137b9aefe99b631505a2064c3c42c9852" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.50.13 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.57.1 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.50.13 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.1 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -179,7 +179,8 @@
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container"><div class="theme-toggle-container theme-toggle-content">
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
@@ -194,28 +195,41 @@
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.queryoptions</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" QueryOptions class for PhotosDB.query """</span>
|
||||
<span></span><span class="sd">""" QueryOptions class for PhotosDB.query """</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">dataclasses</span>
|
||||
<span class="kn">import</span> <span class="nn">datetime</span>
|
||||
<span class="kn">import</span> <span class="nn">io</span>
|
||||
<span class="kn">import</span> <span class="nn">pathlib</span>
|
||||
<span class="kn">import</span> <span class="nn">re</span>
|
||||
<span class="kn">import</span> <span class="nn">sys</span>
|
||||
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">asdict</span><span class="p">,</span> <span class="n">dataclass</span>
|
||||
<span class="kn">from</span> <span class="nn">typing</span> <span class="kn">import</span> <span class="n">Iterable</span><span class="p">,</span> <span class="n">List</span><span class="p">,</span> <span class="n">Optional</span><span class="p">,</span> <span class="n">Tuple</span>
|
||||
|
||||
<span class="kn">import</span> <span class="nn">bitmath</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"QueryOptions"</span><span class="p">]</span>
|
||||
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="n">UUID_PATTERN</span>
|
||||
|
||||
<span class="n">__all__</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"QueryOptions"</span><span class="p">,</span> <span class="s2">"query_options_from_kwargs"</span><span class="p">,</span> <span class="s2">"IncompatibleQueryOptions"</span><span class="p">]</span>
|
||||
|
||||
|
||||
<span class="k">class</span> <span class="nc">IncompatibleQueryOptions</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
|
||||
<span class="sd">"""Incompatible query options"""</span>
|
||||
|
||||
<span class="k">pass</span>
|
||||
|
||||
|
||||
<div class="viewcode-block" id="QueryOptions"><a class="viewcode-back" href="../../reference.html#osxphotos.QueryOptions">[docs]</a><span class="nd">@dataclass</span>
|
||||
<span class="k">class</span> <span class="nc">QueryOptions</span><span class="p">:</span>
|
||||
|
||||
<span class="sd">"""QueryOptions class for PhotosDB.query</span>
|
||||
<span class="sd">"""QueryOptions class for PhotosDB.query</span>
|
||||
|
||||
<span class="sd"> Attributes:</span>
|
||||
<span class="sd"> added_after: search for photos added after a given date</span>
|
||||
<span class="sd"> added_before: search for photos added before a given date</span>
|
||||
<span class="sd"> added_in_last: search for photos added in last X datetime.timedelta</span>
|
||||
<span class="sd"> album: list of album names to search for</span>
|
||||
<span class="sd"> burst_photos: search for burst photos</span>
|
||||
<span class="sd"> burst_photos: include all associated burst photos for photos in query results</span>
|
||||
<span class="sd"> burst: search for burst photos</span>
|
||||
<span class="sd"> cloudasset: search for photos that are managed by iCloud</span>
|
||||
<span class="sd"> deleted_only: search only for deleted photos</span>
|
||||
@@ -257,6 +271,7 @@
|
||||
<span class="sd"> no_title: search for photos with no title</span>
|
||||
<span class="sd"> not_burst: search for non-burst photos</span>
|
||||
<span class="sd"> not_cloudasset: search for photos that are not managed by iCloud</span>
|
||||
<span class="sd"> not_edited: search for photos that have not been edited</span>
|
||||
<span class="sd"> not_favorite: search for non-favorite photos</span>
|
||||
<span class="sd"> not_hdr: search for non-HDR photos</span>
|
||||
<span class="sd"> not_hidden: search for non-hidden photos</span>
|
||||
@@ -290,7 +305,7 @@
|
||||
<span class="sd"> uti: list of UTIs to search for</span>
|
||||
<span class="sd"> uuid: list of uuids to search for</span>
|
||||
<span class="sd"> year: search for photos taken in a given year</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="n">added_after</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">added_before</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="n">datetime</span><span class="o">.</span><span class="n">datetime</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -339,6 +354,7 @@
|
||||
<span class="n">no_title</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_burst</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_cloudasset</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_edited</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_favorite</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_hdr</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
<span class="n">not_hidden</span><span class="p">:</span> <span class="n">Optional</span><span class="p">[</span><span class="nb">bool</span><span class="p">]</span> <span class="o">=</span> <span class="kc">None</span>
|
||||
@@ -376,6 +392,168 @@
|
||||
|
||||
<span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="k">return</span> <span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span></div>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">query_options_from_kwargs</span><span class="p">(</span><span class="o">**</span><span class="n">kwargs</span><span class="p">)</span> <span class="o">-></span> <span class="n">QueryOptions</span><span class="p">:</span>
|
||||
<span class="sd">"""Validate query options and create a QueryOptions instance.</span>
|
||||
<span class="sd"> Note: this will block on stdin if uuid_from_file is set to "-"</span>
|
||||
<span class="sd"> so it is best to call function before creating the PhotosDB instance</span>
|
||||
<span class="sd"> so that the validation of query options can happen before the database</span>
|
||||
<span class="sd"> is loaded.</span>
|
||||
<span class="sd"> """</span>
|
||||
<span class="c1"># sanity check input args</span>
|
||||
<span class="n">nonexclusive</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="s2">"added_after"</span><span class="p">,</span>
|
||||
<span class="s2">"added_before"</span><span class="p">,</span>
|
||||
<span class="s2">"added_in_last"</span><span class="p">,</span>
|
||||
<span class="s2">"album"</span><span class="p">,</span>
|
||||
<span class="s2">"duplicate"</span><span class="p">,</span>
|
||||
<span class="s2">"exif"</span><span class="p">,</span>
|
||||
<span class="s2">"external_edit"</span><span class="p">,</span>
|
||||
<span class="s2">"folder"</span><span class="p">,</span>
|
||||
<span class="s2">"from_date"</span><span class="p">,</span>
|
||||
<span class="s2">"from_time"</span><span class="p">,</span>
|
||||
<span class="s2">"has_raw"</span><span class="p">,</span>
|
||||
<span class="s2">"keyword"</span><span class="p">,</span>
|
||||
<span class="s2">"label"</span><span class="p">,</span>
|
||||
<span class="s2">"max_size"</span><span class="p">,</span>
|
||||
<span class="s2">"min_size"</span><span class="p">,</span>
|
||||
<span class="s2">"name"</span><span class="p">,</span>
|
||||
<span class="s2">"person"</span><span class="p">,</span>
|
||||
<span class="s2">"query_eval"</span><span class="p">,</span>
|
||||
<span class="s2">"query_function"</span><span class="p">,</span>
|
||||
<span class="s2">"regex"</span><span class="p">,</span>
|
||||
<span class="s2">"selected"</span><span class="p">,</span>
|
||||
<span class="s2">"to_date"</span><span class="p">,</span>
|
||||
<span class="s2">"to_time"</span><span class="p">,</span>
|
||||
<span class="s2">"uti"</span><span class="p">,</span>
|
||||
<span class="s2">"uuid"</span><span class="p">,</span>
|
||||
<span class="s2">"uuid_from_file"</span><span class="p">,</span>
|
||||
<span class="s2">"year"</span><span class="p">,</span>
|
||||
<span class="p">]</span>
|
||||
<span class="n">exclusive</span> <span class="o">=</span> <span class="p">[</span>
|
||||
<span class="p">(</span><span class="s2">"burst"</span><span class="p">,</span> <span class="s2">"not_burst"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"cloudasset"</span><span class="p">,</span> <span class="s2">"not_cloudasset"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"edited"</span><span class="p">,</span> <span class="s2">"not_edited"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"favorite"</span><span class="p">,</span> <span class="s2">"not_favorite"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"has_comment"</span><span class="p">,</span> <span class="s2">"no_comment"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"has_likes"</span><span class="p">,</span> <span class="s2">"no_likes"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"hdr"</span><span class="p">,</span> <span class="s2">"not_hdr"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"hidden"</span><span class="p">,</span> <span class="s2">"not_hidden"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"in_album"</span><span class="p">,</span> <span class="s2">"not_in_album"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"incloud"</span><span class="p">,</span> <span class="s2">"not_incloud"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"is_reference"</span><span class="p">,</span> <span class="s2">"not_reference"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"keyword"</span><span class="p">,</span> <span class="s2">"no_keyword"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"live"</span><span class="p">,</span> <span class="s2">"not_live"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"location"</span><span class="p">,</span> <span class="s2">"no_location"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"missing"</span><span class="p">,</span> <span class="s2">"not_missing"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"only_photos"</span><span class="p">,</span> <span class="s2">"only_movies"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"panorama"</span><span class="p">,</span> <span class="s2">"not_panorama"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"portrait"</span><span class="p">,</span> <span class="s2">"not_portrait"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"screenshot"</span><span class="p">,</span> <span class="s2">"not_screenshot"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"selfie"</span><span class="p">,</span> <span class="s2">"not_selfie"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"shared"</span><span class="p">,</span> <span class="s2">"not_shared"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"slow_mo"</span><span class="p">,</span> <span class="s2">"not_slow_mo"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"time_lapse"</span><span class="p">,</span> <span class="s2">"not_time_lapse"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"deleted"</span><span class="p">,</span> <span class="s2">"not_deleted"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"deleted"</span><span class="p">,</span> <span class="s2">"deleted_only"</span><span class="p">),</span>
|
||||
<span class="p">(</span><span class="s2">"deleted_only"</span><span class="p">,</span> <span class="s2">"not_deleted"</span><span class="p">),</span>
|
||||
<span class="p">]</span>
|
||||
<span class="c1"># TODO: add option to validate requiring at least one query arg</span>
|
||||
<span class="k">for</span> <span class="n">arg</span><span class="p">,</span> <span class="n">not_arg</span> <span class="ow">in</span> <span class="n">exclusive</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">arg</span><span class="p">)</span> <span class="ow">and</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">not_arg</span><span class="p">):</span>
|
||||
<span class="n">arg</span> <span class="o">=</span> <span class="n">arg</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"_"</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">)</span>
|
||||
<span class="n">not_arg</span> <span class="o">=</span> <span class="n">not_arg</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="s2">"_"</span><span class="p">,</span> <span class="s2">"-"</span><span class="p">)</span>
|
||||
<span class="k">raise</span> <span class="n">IncompatibleQueryOptions</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"Incompatible query options: --</span><span class="si">{</span><span class="n">arg</span><span class="si">}</span><span class="s2"> and --</span><span class="si">{</span><span class="n">not_arg</span><span class="si">}</span><span class="s2"> are mutually exclusive"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="c1"># some options like title can be specified multiple times</span>
|
||||
<span class="c1"># check if any of them are specified along with their no_ counterpart</span>
|
||||
<span class="n">exclusive_multi_options</span> <span class="o">=</span> <span class="p">[</span><span class="s2">"title"</span><span class="p">,</span> <span class="s2">"description"</span><span class="p">,</span> <span class="s2">"place"</span><span class="p">,</span> <span class="s2">"keyword"</span><span class="p">]</span>
|
||||
<span class="k">for</span> <span class="n">option</span> <span class="ow">in</span> <span class="n">exclusive_multi_options</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">option</span><span class="p">)</span> <span class="ow">and</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"no_</span><span class="si">{option}</span><span class="s2">"</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="n">IncompatibleQueryOptions</span><span class="p">(</span>
|
||||
<span class="sa">f</span><span class="s2">"--</span><span class="si">{</span><span class="n">option</span><span class="si">}</span><span class="s2"> and --no-</span><span class="si">{</span><span class="n">option</span><span class="si">}</span><span class="s2"> are mutually exclusive"</span>
|
||||
<span class="p">)</span>
|
||||
|
||||
<span class="n">include_photos</span> <span class="o">=</span> <span class="kc">True</span>
|
||||
<span class="n">include_movies</span> <span class="o">=</span> <span class="kc">True</span> <span class="c1"># default searches for everything</span>
|
||||
<span class="k">if</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"only_movies"</span><span class="p">):</span>
|
||||
<span class="n">include_photos</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
<span class="k">if</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"only_photos"</span><span class="p">):</span>
|
||||
<span class="n">include_movies</span> <span class="o">=</span> <span class="kc">False</span>
|
||||
|
||||
<span class="c1"># load UUIDs if necessary and append to any uuids passed with --uuid</span>
|
||||
<span class="n">uuids</span> <span class="o">=</span> <span class="nb">list</span><span class="p">(</span><span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"uuid"</span><span class="p">,</span> <span class="p">[]))</span> <span class="c1"># Click option is a tuple</span>
|
||||
<span class="k">if</span> <span class="n">uuid_from_file</span> <span class="o">:=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"uuid_from_file"</span><span class="p">):</span>
|
||||
<span class="n">uuids</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">load_uuid_from_file</span><span class="p">(</span><span class="n">uuid_from_file</span><span class="p">))</span>
|
||||
<span class="n">uuids</span> <span class="o">=</span> <span class="nb">tuple</span><span class="p">(</span><span class="n">uuids</span><span class="p">)</span>
|
||||
|
||||
<span class="n">query_fields</span> <span class="o">=</span> <span class="p">[</span><span class="n">field</span><span class="o">.</span><span class="n">name</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">dataclasses</span><span class="o">.</span><span class="n">fields</span><span class="p">(</span><span class="n">QueryOptions</span><span class="p">)]</span>
|
||||
<span class="n">query_dict</span> <span class="o">=</span> <span class="p">{</span><span class="n">field</span><span class="p">:</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">field</span><span class="p">)</span> <span class="k">for</span> <span class="n">field</span> <span class="ow">in</span> <span class="n">query_fields</span><span class="p">}</span>
|
||||
<span class="n">query_dict</span><span class="p">[</span><span class="s2">"photos"</span><span class="p">]</span> <span class="o">=</span> <span class="n">include_photos</span>
|
||||
<span class="n">query_dict</span><span class="p">[</span><span class="s2">"movies"</span><span class="p">]</span> <span class="o">=</span> <span class="n">include_movies</span>
|
||||
<span class="n">query_dict</span><span class="p">[</span><span class="s2">"uuid"</span><span class="p">]</span> <span class="o">=</span> <span class="n">uuids</span>
|
||||
<span class="n">query_dict</span><span class="p">[</span><span class="s2">"function"</span><span class="p">]</span> <span class="o">=</span> <span class="n">kwargs</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">"query_function"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">return</span> <span class="n">QueryOptions</span><span class="p">(</span><span class="o">**</span><span class="n">query_dict</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">load_uuid_from_file</span><span class="p">(</span><span class="n">filename</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> Load UUIDs from file.</span>
|
||||
<span class="sd"> Does not validate UUIDs but does validate that the UUIDs are in the correct format.</span>
|
||||
<span class="sd"> Format is 1 UUID per line, any line beginning with # is ignored.</span>
|
||||
<span class="sd"> Whitespace is stripped.</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> filename: file name of the file containing UUIDs</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> list of UUIDs or empty list of no UUIDs in file</span>
|
||||
|
||||
<span class="sd"> Raises:</span>
|
||||
<span class="sd"> FileNotFoundError if file does not exist</span>
|
||||
<span class="sd"> ValueError if UUID is not in correct format</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="k">if</span> <span class="n">filename</span> <span class="o">==</span> <span class="s2">"-"</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">_load_uuid_from_stream</span><span class="p">(</span><span class="n">sys</span><span class="o">.</span><span class="n">stdin</span><span class="p">)</span>
|
||||
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">pathlib</span><span class="o">.</span><span class="n">Path</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span><span class="o">.</span><span class="n">is_file</span><span class="p">():</span>
|
||||
<span class="k">raise</span> <span class="ne">FileNotFoundError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Could not find file </span><span class="si">{</span><span class="n">filename</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
|
||||
<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s2">"r"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="n">_load_uuid_from_stream</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
|
||||
|
||||
|
||||
<span class="k">def</span> <span class="nf">_load_uuid_from_stream</span><span class="p">(</span><span class="n">stream</span><span class="p">:</span> <span class="n">io</span><span class="o">.</span><span class="n">IOBase</span><span class="p">)</span> <span class="o">-></span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
|
||||
<span class="sd">"""</span>
|
||||
<span class="sd"> Load UUIDs from stream.</span>
|
||||
<span class="sd"> Does not validate UUIDs but does validate that the UUIDs are in the correct format.</span>
|
||||
<span class="sd"> Format is 1 UUID per line, any line beginning with # is ignored.</span>
|
||||
<span class="sd"> Whitespace is stripped.</span>
|
||||
|
||||
<span class="sd"> Arguments:</span>
|
||||
<span class="sd"> filename: file name of the file containing UUIDs</span>
|
||||
|
||||
<span class="sd"> Returns:</span>
|
||||
<span class="sd"> list of UUIDs or empty list of no UUIDs in file</span>
|
||||
|
||||
<span class="sd"> Raises:</span>
|
||||
<span class="sd"> ValueError if UUID is not in correct format</span>
|
||||
<span class="sd"> """</span>
|
||||
|
||||
<span class="n">uuid</span> <span class="o">=</span> <span class="p">[]</span>
|
||||
<span class="k">for</span> <span class="n">line</span> <span class="ow">in</span> <span class="n">stream</span><span class="p">:</span>
|
||||
<span class="n">line</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">strip</span><span class="p">()</span>
|
||||
<span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">line</span><span class="p">)</span> <span class="ow">and</span> <span class="n">line</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">!=</span> <span class="s2">"#"</span><span class="p">:</span>
|
||||
<span class="k">if</span> <span class="ow">not</span> <span class="n">re</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="sa">f</span><span class="s2">"^</span><span class="si">{</span><span class="n">UUID_PATTERN</span><span class="si">}</span><span class="s2">$"</span><span class="p">,</span> <span class="n">line</span><span class="p">):</span>
|
||||
<span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="sa">f</span><span class="s2">"Invalid UUID: </span><span class="si">{</span><span class="n">line</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
|
||||
<span class="n">line</span> <span class="o">=</span> <span class="n">line</span><span class="o">.</span><span class="n">upper</span><span class="p">()</span>
|
||||
<span class="n">uuid</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">uuid</span>
|
||||
</pre></div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -413,7 +591,9 @@
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
|
||||
@@ -1,39 +1,203 @@
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<!DOCTYPE html>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.scoreinfo - osxphotos 0.56.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
|
||||
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>osxphotos.scoreinfo — osxphotos 0.47.9 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/alabaster.css" />
|
||||
<script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<link rel="index" title="Index" href="../../genindex.html" />
|
||||
<link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<link rel="stylesheet" href="../../_static/custom.css" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
--color-code-background: #f8f8f8;
|
||||
--color-code-foreground: black;
|
||||
|
||||
}
|
||||
@media not print {
|
||||
body[data-theme="dark"] {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body:not([data-theme="light"]) {
|
||||
--color-code-background: #202020;
|
||||
--color-code-foreground: #d0d0d0;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
</style></head>
|
||||
<body>
|
||||
|
||||
<script>
|
||||
document.body.dataset.theme = localStorage.getItem("theme") || "auto";
|
||||
</script>
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style="display: none;">
|
||||
<symbol id="svg-toc" viewBox="0 0 24 24">
|
||||
<title>Contents</title>
|
||||
<svg stroke="currentColor" fill="currentColor" stroke-width="0" viewBox="0 0 1024 1024">
|
||||
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM115.4 518.9L271.7 642c5.8 4.6 14.4.5 14.4-6.9V388.9c0-7.4-8.5-11.5-14.4-6.9L115.4 505.1a8.74 8.74 0 0 0 0 13.8z"/>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-menu" viewBox="0 0 24 24">
|
||||
<title>Menu</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-menu">
|
||||
<line x1="3" y1="12" x2="21" y2="12"></line>
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<line x1="3" y1="18" x2="21" y2="18"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-arrow-right" viewBox="0 0 24 24">
|
||||
<title>Expand</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather-chevron-right">
|
||||
<polyline points="9 18 15 12 9 6"></polyline>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun" viewBox="0 0 24 24">
|
||||
<title>Light mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="feather-sun">
|
||||
<circle cx="12" cy="12" r="5"></circle>
|
||||
<line x1="12" y1="1" x2="12" y2="3"></line>
|
||||
<line x1="12" y1="21" x2="12" y2="23"></line>
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
||||
<line x1="1" y1="12" x2="3" y2="12"></line>
|
||||
<line x1="21" y1="12" x2="23" y2="12"></line>
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-moon" viewBox="0 0 24 24">
|
||||
<title>Dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-moon">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 0 0 7.92 12.446a9 9 0 1 1 -8.313 -12.454z" />
|
||||
</svg>
|
||||
</symbol>
|
||||
<symbol id="svg-sun-half" viewBox="0 0 24 24">
|
||||
<title>Auto light/dark mode</title>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
||||
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" class="icon-tabler-shadow">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M13 12h5" />
|
||||
<path d="M13 15h4" />
|
||||
<path d="M13 18h1" />
|
||||
<path d="M13 9h4" />
|
||||
<path d="M13 6h1" />
|
||||
</svg>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
<input type="checkbox" class="sidebar-toggle" name="__navigation" id="__navigation">
|
||||
<input type="checkbox" class="sidebar-toggle" name="__toc" id="__toc">
|
||||
<label class="overlay sidebar-overlay" for="__navigation">
|
||||
<div class="visually-hidden">Hide navigation sidebar</div>
|
||||
</label>
|
||||
<label class="overlay toc-overlay" for="__toc">
|
||||
<div class="visually-hidden">Hide table of contents sidebar</div>
|
||||
</label>
|
||||
|
||||
|
||||
|
||||
<div class="page">
|
||||
<header class="mobile-header">
|
||||
<div class="header-left">
|
||||
<label class="nav-overlay-icon" for="__navigation">
|
||||
<div class="visually-hidden">Toggle site navigation sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-menu"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.4 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-header-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
</header>
|
||||
<aside class="sidebar-drawer">
|
||||
<div class="sidebar-container">
|
||||
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.9, maximum-scale=0.9" />
|
||||
|
||||
</head><body>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.4 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
<input type="hidden" name="check_keywords" value="yes">
|
||||
<input type="hidden" name="area" value="default">
|
||||
</form>
|
||||
<div id="searchbox"></div><div class="sidebar-scroll"><div class="sidebar-tree">
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">OSXPhotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">OSXPhotos Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">OSXPhotos python API</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="document">
|
||||
<div class="documentwrapper">
|
||||
<div class="bodywrapper">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body" role="main">
|
||||
|
||||
<h1>Source code for osxphotos.scoreinfo</h1><div class="highlight"><pre>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</aside>
|
||||
<div class="main">
|
||||
<div class="content">
|
||||
<div class="article-container">
|
||||
<a href="#" class="back-to-top muted-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8v12z"></path>
|
||||
</svg>
|
||||
<span>Back to top</span>
|
||||
</a>
|
||||
<div class="content-icon-container">
|
||||
<div class="theme-toggle-container theme-toggle-content">
|
||||
<button class="theme-toggle">
|
||||
<div class="visually-hidden">Toggle Light / Dark / Auto color theme</div>
|
||||
<svg class="theme-icon-when-auto"><use href="#svg-sun-half"></use></svg>
|
||||
<svg class="theme-icon-when-dark"><use href="#svg-moon"></use></svg>
|
||||
<svg class="theme-icon-when-light"><use href="#svg-sun"></use></svg>
|
||||
</button>
|
||||
</div>
|
||||
<label class="toc-overlay-icon toc-content-icon no-toc" for="__toc">
|
||||
<div class="visually-hidden">Toggle table of contents sidebar</div>
|
||||
<i class="icon"><svg><use href="#svg-toc"></use></svg></i>
|
||||
</label>
|
||||
</div>
|
||||
<article role="main">
|
||||
<h1>Source code for osxphotos.scoreinfo</h1><div class="highlight"><pre>
|
||||
<span></span><span class="sd">""" ScoreInfo class to expose computed score info from the library """</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span>
|
||||
<span class="kn">from</span> <span class="nn">dataclasses</span> <span class="kn">import</span> <span class="n">dataclass</span><span class="p">,</span> <span class="n">asdict</span>
|
||||
|
||||
<span class="kn">from</span> <span class="nn">._constants</span> <span class="kn">import</span> <span class="n">_PHOTOS_4_VERSION</span>
|
||||
|
||||
@@ -70,74 +234,53 @@
|
||||
<span class="n">tastefully_blurred</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_chosen_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_framed_subject</span><span class="p">:</span> <span class="nb">float</span>
|
||||
<span class="n">well_timed_shot</span><span class="p">:</span> <span class="nb">float</span></div>
|
||||
</pre></div>
|
||||
<span class="n">well_timed_shot</span><span class="p">:</span> <span class="nb">float</span>
|
||||
|
||||
</div>
|
||||
<div class="viewcode-block" id="ScoreInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.ScoreInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Return ScoreInfo as a dict"""</span>
|
||||
<span class="k">return</span> <span class="n">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span></div></div>
|
||||
</pre></div>
|
||||
</article>
|
||||
</div>
|
||||
<footer>
|
||||
|
||||
<div class="related-pages">
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="sphinxsidebar" role="navigation" aria-label="main navigation">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
<h1 class="logo"><a href="../../index.html">osxphotos</a></h1>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h3>Navigation</h3>
|
||||
<ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../overview.html">osxphotos</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../tutorial.html">Tutorial</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../cli.html">osxphotos command line interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="../../reference.html">osxphotos package</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="relations">
|
||||
<h3>Related Topics</h3>
|
||||
<ul>
|
||||
<li><a href="../../index.html">Documentation overview</a><ul>
|
||||
<li><a href="../index.html">Module code</a><ul>
|
||||
</ul></li>
|
||||
</ul></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="searchbox" style="display: none" role="search">
|
||||
<h3 id="searchlabel">Quick search</h3>
|
||||
<div class="searchformwrapper">
|
||||
<form class="search" action="../../search.html" method="get">
|
||||
<input type="text" name="q" aria-labelledby="searchlabel" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"/>
|
||||
<input type="submit" value="Go" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<script>$('#searchbox').show(0);</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="bottom-of-page">
|
||||
<div class="left-details">
|
||||
<div class="copyright">
|
||||
Copyright © 2021, Rhet Turnbull
|
||||
</div>
|
||||
Made with <a href="https://www.sphinx-doc.org/">Sphinx</a> and <a class="muted-link" href="https://pradyunsg.me">@pradyunsg</a>'s
|
||||
|
||||
<a href="https://github.com/pradyunsg/furo">Furo</a>
|
||||
|
||||
</div>
|
||||
<div class="right-details">
|
||||
<div class="icons">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="clearer"></div>
|
||||
|
||||
</footer>
|
||||
</div>
|
||||
<div class="footer">
|
||||
©2021, Rhet Turnbull.
|
||||
<aside class="toc-drawer no-toc">
|
||||
|
||||
|
|
||||
Powered by <a href="http://sphinx-doc.org/">Sphinx 4.4.0</a>
|
||||
& <a href="https://github.com/bitprophet/alabaster">Alabaster 0.7.12</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
|
||||
</aside>
|
||||
</div>
|
||||
</div><script data-url_root="../../" id="documentation_options" src="../../_static/documentation_options.js"></script>
|
||||
<script src="../../_static/jquery.js"></script>
|
||||
<script src="../../_static/underscore.js"></script>
|
||||
<script src="../../_static/_sphinx_javascript_frameworks_compat.js"></script>
|
||||
<script src="../../_static/doctools.js"></script>
|
||||
<script src="../../_static/sphinx_highlight.js"></script>
|
||||
<script src="../../_static/scripts/furo.js"></script>
|
||||
<script src="../../_static/clipboard.min.js"></script>
|
||||
<script src="../../_static/copybutton.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="../../genindex.html" /><link rel="search" title="Search" href="../../search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos.searchinfo - osxphotos 0.54.1 documentation</title>
|
||||
<title>osxphotos.searchinfo - osxphotos 0.56.4 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="../../_static/copybutton.css" />
|
||||
@@ -123,7 +123,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.54.1 documentation</div></a>
|
||||
<a href="../../index.html"><div class="brand">osxphotos 0.56.4 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -146,7 +146,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="../../index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.1 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.56.4 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="../../search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -336,6 +336,13 @@
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_categories</span><span class="o">.</span><span class="n">DETECTED_TEXT</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">text_found</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""Returns True if photos has detected text (macOS 13+ / Photos 8+ only)"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o"><</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="p">[]</span>
|
||||
<span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_categories</span><span class="o">.</span><span class="n">TEXT_FOUND</span><span class="p">)</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">camera</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns camera name (macOS 13+ / Photos 8+ only)"""</span>
|
||||
@@ -344,10 +351,18 @@
|
||||
<span class="n">camera</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_categories</span><span class="o">.</span><span class="n">CAMERA</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">camera</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">camera</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">source</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""returns source of the photo (e.g. "Messages", "Safar", etc) (macOS 13+ / Photos 8+ only)"""</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">_photo</span><span class="o">.</span><span class="n">_db</span><span class="o">.</span><span class="n">_photos_ver</span> <span class="o"><</span> <span class="mi">8</span><span class="p">:</span>
|
||||
<span class="k">return</span> <span class="s2">""</span>
|
||||
<span class="n">source</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">_categories</span><span class="o">.</span><span class="n">SOURCE</span><span class="p">)</span>
|
||||
<span class="k">return</span> <span class="n">source</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">if</span> <span class="n">source</span> <span class="k">else</span> <span class="s2">""</span>
|
||||
|
||||
<span class="nd">@property</span>
|
||||
<span class="k">def</span> <span class="nf">all</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return all search info properties in a single list"""</span>
|
||||
<span class="nb">all</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="n">all_</span> <span class="o">=</span> <span class="p">(</span>
|
||||
<span class="bp">self</span><span class="o">.</span><span class="n">labels</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">place_names</span>
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">streets</span>
|
||||
@@ -362,23 +377,23 @@
|
||||
<span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">detected_text</span>
|
||||
<span class="p">)</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">city</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">state_abbreviation</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">country</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">month</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">year</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">season</span><span class="p">]</span>
|
||||
<span class="k">if</span> <span class="bp">self</span><span class="o">.</span><span class="n">camera</span><span class="p">:</span>
|
||||
<span class="nb">all</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">camera</span><span class="p">]</span>
|
||||
<span class="n">all_</span> <span class="o">+=</span> <span class="p">[</span><span class="bp">self</span><span class="o">.</span><span class="n">camera</span><span class="p">]</span>
|
||||
|
||||
<span class="k">return</span> <span class="nb">all</span>
|
||||
<span class="k">return</span> <span class="n">all_</span>
|
||||
|
||||
<div class="viewcode-block" id="SearchInfo.asdict"><a class="viewcode-back" href="../../reference.html#osxphotos.SearchInfo.asdict">[docs]</a> <span class="k">def</span> <span class="nf">asdict</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
|
||||
<span class="sd">"""return dict of search info"""</span>
|
||||
@@ -403,6 +418,7 @@
|
||||
<span class="s2">"media_types"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">media_types</span><span class="p">,</span>
|
||||
<span class="s2">"detected_text"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">detected_text</span><span class="p">,</span>
|
||||
<span class="s2">"camera"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">camera</span><span class="p">,</span>
|
||||
<span class="s2">"source"</span><span class="p">:</span> <span class="bp">self</span><span class="o">.</span><span class="n">source</span><span class="p">,</span>
|
||||
<span class="p">}</span></div>
|
||||
|
||||
<span class="k">def</span> <span class="nf">_get_text_for_category</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">category</span><span class="p">):</span>
|
||||
|
||||
2977
docs/_sources/API_README.rst.txt
Normal file
2977
docs/_sources/API_README.rst.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
OSXPhotos python API
|
||||
====================
|
||||
OSXPhotos Python Reference
|
||||
==========================
|
||||
|
||||
.. automodule:: osxphotos
|
||||
:members:
|
||||
@@ -322,6 +322,8 @@ Template Substitutions
|
||||
- A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'
|
||||
* - {id}
|
||||
- A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc.
|
||||
* - {counter}
|
||||
- A sequential counter, starting at 0, that increments each time it is evaluated.To start counting at a value other than 0, append append '(starting_value)' to the field name.For example, to start counting at 1 instead of 0: '{counter(1)}'.May be formatted using a python string format code.For example, to format as a 5-digit integer and pad with zeros, use '{counter:05d(1)}'which results in 00001, 00002, 00003...etc.You may also specify a stop value which causes the counter to reset to the starting valuewhen the stop value is reached and a step size which causes the counter to increment bythe specified value instead of 1. Use the format '{counter(start,stop,step)}' where start,stop, and step are integers. For example, to count from 1 to 10 by 2, use '{counter(1,11,2)}'.Note that the counter stops counting when the stop value is reached and does not return thestop value. Start, stop, and step are optional and may be omitted. For example, to countfrom 0 by 2s, use '{counter(,,2)}'.You may create an arbitrary number of counters by appending a unique name to the field namepreceded by a period: '{counter.a}', '{counter.b}', etc. Each counter will have its own stateand will start at 0 and increment by 1 unless otherwise specified. Note: {counter} is not suitable for use with 'export' and '--update' as the counter associated with a photo may change between export sessions. See also {id}.
|
||||
* - {album_seq}
|
||||
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
|
||||
* - {folder_album_seq}
|
||||
@@ -357,7 +359,7 @@ Template Substitutions
|
||||
* - {tab}
|
||||
- :A tab: '\t'
|
||||
* - {osxphotos_version}
|
||||
- The osxphotos version, e.g. '0.54.4'
|
||||
- The osxphotos version, e.g. '0.57.3'
|
||||
* - {osxphotos_cmd_line}
|
||||
- The full command line used to run osxphotos
|
||||
* - {album}
|
||||
|
||||
3
docs/_static/copybutton.css
vendored
3
docs/_static/copybutton.css
vendored
@@ -35,7 +35,8 @@ div.highlight {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.highlight:hover button.copybtn {
|
||||
/* Show the copybutton */
|
||||
.highlight:hover button.copybtn, button.copybtn.success {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
42
docs/_static/copybutton.js
vendored
42
docs/_static/copybutton.js
vendored
@@ -102,18 +102,25 @@ const clearSelection = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// Changes tooltip text for two seconds, then changes it back
|
||||
// Changes tooltip text for a moment, then changes it back
|
||||
// We want the timeout of our `success` class to be a bit shorter than the
|
||||
// tooltip and icon change, so that we can hide the icon before changing back.
|
||||
var timeoutIcon = 2000;
|
||||
var timeoutSuccessClass = 1500;
|
||||
|
||||
const temporarilyChangeTooltip = (el, oldText, newText) => {
|
||||
el.setAttribute('data-tooltip', newText)
|
||||
el.classList.add('success')
|
||||
setTimeout(() => el.setAttribute('data-tooltip', oldText), 2000)
|
||||
setTimeout(() => el.classList.remove('success'), 2000)
|
||||
// Remove success a little bit sooner than we change the tooltip
|
||||
// So that we can use CSS to hide the copybutton first
|
||||
setTimeout(() => el.classList.remove('success'), timeoutSuccessClass)
|
||||
setTimeout(() => el.setAttribute('data-tooltip', oldText), timeoutIcon)
|
||||
}
|
||||
|
||||
// Changes the copy button icon for two seconds, then changes it back
|
||||
const temporarilyChangeIcon = (el) => {
|
||||
el.innerHTML = iconCheck;
|
||||
setTimeout(() => {el.innerHTML = iconCopy}, 2000)
|
||||
setTimeout(() => {el.innerHTML = iconCopy}, timeoutIcon)
|
||||
}
|
||||
|
||||
const addCopyButtonToCodeCells = () => {
|
||||
@@ -125,7 +132,8 @@ const addCopyButtonToCodeCells = () => {
|
||||
}
|
||||
|
||||
// Add copybuttons to all of our code cells
|
||||
const codeCells = document.querySelectorAll('div.highlight pre')
|
||||
const COPYBUTTON_SELECTOR = 'div.highlight pre';
|
||||
const codeCells = document.querySelectorAll(COPYBUTTON_SELECTOR)
|
||||
codeCells.forEach((codeCell, index) => {
|
||||
const id = codeCellId(index)
|
||||
codeCell.setAttribute('id', id)
|
||||
@@ -141,10 +149,25 @@ function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
@@ -199,7 +222,12 @@ function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onl
|
||||
|
||||
var copyTargetText = (trigger) => {
|
||||
var target = document.querySelector(trigger.attributes['data-clipboard-target'].value);
|
||||
return formatCopyText(target.innerText, '', false, true, true, true, '', '')
|
||||
|
||||
// get filtered text
|
||||
let exclude = '.linenos, .gp';
|
||||
|
||||
let text = filterText(target, exclude);
|
||||
return formatCopyText(text, '', false, true, true, true, '', '')
|
||||
}
|
||||
|
||||
// Initialize with a callback so we can modify the text before copy
|
||||
|
||||
17
docs/_static/copybutton_funcs.js
vendored
17
docs/_static/copybutton_funcs.js
vendored
@@ -2,10 +2,25 @@ function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes excluded text from a Node.
|
||||
*
|
||||
* @param {Node} target Node to filter.
|
||||
* @param {string} exclude CSS selector of nodes to exclude.
|
||||
* @returns {DOMString} Text from `target` with text removed.
|
||||
*/
|
||||
export function filterText(target, exclude) {
|
||||
const clone = target.cloneNode(true); // clone as to not modify the live DOM
|
||||
if (exclude) {
|
||||
// remove excluded nodes
|
||||
clone.querySelectorAll(exclude).forEach(node => node.remove());
|
||||
}
|
||||
return clone.innerText;
|
||||
}
|
||||
|
||||
// Callback when a copy button is clicked. Will be passed the node that was clicked
|
||||
// should then grab the text and replace pieces of text that shouldn't be used in output
|
||||
export function formatCopyText(textContent, copybuttonPromptText, isRegexp = false, onlyCopyPromptLines = true, removePrompts = true, copyEmptyLines = true, lineContinuationChar = "", hereDocDelim = "") {
|
||||
|
||||
var regexp;
|
||||
var match;
|
||||
|
||||
|
||||
2
docs/_static/documentation_options.js
vendored
2
docs/_static/documentation_options.js
vendored
@@ -1,6 +1,6 @@
|
||||
var DOCUMENTATION_OPTIONS = {
|
||||
URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'),
|
||||
VERSION: '0.54.4',
|
||||
VERSION: '0.57.3',
|
||||
LANGUAGE: 'en',
|
||||
COLLAPSE_INDEX: false,
|
||||
BUILDER: 'html',
|
||||
|
||||
1685
docs/cli.html
1685
docs/cli.html
File diff suppressed because it is too large
Load Diff
1128
docs/genindex.html
1128
docs/genindex.html
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos" href="overview.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>osxphotos 0.54.4 documentation</title>
|
||||
<title>osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="#"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="#"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="#">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -161,7 +161,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -236,6 +236,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="cli.html#osxphotos">osxphotos</a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-about">about</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-add-locations">add-locations</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-albums">albums</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-diff">diff</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-docs">docs</a></li>
|
||||
@@ -257,7 +258,9 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-query">query</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-repl">repl</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-run">run</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-show">show</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-snap">snap</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-sync">sync</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-theme">theme</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-timewarp">timewarp</a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="cli.html#osxphotos-tutorial">tutorial</a></li>
|
||||
@@ -278,7 +281,7 @@
|
||||
<li class="toctree-l2"><a class="reference internal" href="package_overview.html#using-the-osxphotos-cli-to-run-python-code">Using the osxphotos CLI to run python code</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a><ul>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a><ul>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo"><code class="docutils literal notranslate"><span class="pre">AlbumInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.folder_list"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_list</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.AlbumInfo.folder_names"><code class="docutils literal notranslate"><span class="pre">AlbumInfo.folder_names</span></code></a></li>
|
||||
@@ -361,6 +364,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.timeout"><code class="docutils literal notranslate"><span class="pre">ExportOptions.timeout</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.touch_file"><code class="docutils literal notranslate"><span class="pre">ExportOptions.touch_file</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.update"><code class="docutils literal notranslate"><span class="pre">ExportOptions.update</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.update_errors"><code class="docutils literal notranslate"><span class="pre">ExportOptions.update_errors</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_albums_as_keywords"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_albums_as_keywords</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_persons_as_keywords"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_persons_as_keywords</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ExportOptions.use_photos_export"><code class="docutils literal notranslate"><span class="pre">ExportOptions.use_photos_export</span></code></a></li>
|
||||
@@ -419,8 +423,11 @@
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.PersonInfo"><code class="docutils literal notranslate"><span class="pre">PersonInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.asdict"><code class="docutils literal notranslate"><span class="pre">PersonInfo.asdict()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.face_info"><code class="docutils literal notranslate"><span class="pre">PersonInfo.face_info</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.favorite"><code class="docutils literal notranslate"><span class="pre">PersonInfo.favorite</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.feature_less"><code class="docutils literal notranslate"><span class="pre">PersonInfo.feature_less</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.json"><code class="docutils literal notranslate"><span class="pre">PersonInfo.json()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.photos"><code class="docutils literal notranslate"><span class="pre">PersonInfo.photos</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PersonInfo.sort_order"><code class="docutils literal notranslate"><span class="pre">PersonInfo.sort_order</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.PhotoExporter"><code class="docutils literal notranslate"><span class="pre">PhotoExporter</span></code></a><ul>
|
||||
@@ -441,7 +448,9 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.burst_key"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.burst_key</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.burst_photos"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.burst_photos</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.burst_selected"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.burst_selected</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.cloud_guid"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.cloud_guid</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.cloud_metadata"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.cloud_metadata</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.cloud_owner_hashed_id"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.cloud_owner_hashed_id</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.comments"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.comments</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.date"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.date</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.date_added"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.date_added</span></code></a></li>
|
||||
@@ -457,6 +466,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.face_info"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.face_info</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.favorite"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.favorite</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.filename"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.filename</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.fingerprint"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.fingerprint</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.has_raw"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.has_raw</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.hasadjustments"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.hasadjustments</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotoInfo.hdr"><code class="docutils literal notranslate"><span class="pre">PhotoInfo.hdr</span></code></a></li>
|
||||
@@ -568,6 +578,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.persons_as_dict"><code class="docutils literal notranslate"><span class="pre">PhotosDB.persons_as_dict</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.photos"><code class="docutils literal notranslate"><span class="pre">PhotosDB.photos()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.photos_by_uuid"><code class="docutils literal notranslate"><span class="pre">PhotosDB.photos_by_uuid()</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.photos_version"><code class="docutils literal notranslate"><span class="pre">PhotosDB.photos_version</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.project_info"><code class="docutils literal notranslate"><span class="pre">PhotosDB.project_info</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.PhotosDB.query"><code class="docutils literal notranslate"><span class="pre">PhotosDB.query()</span></code></a></li>
|
||||
</ul>
|
||||
@@ -621,6 +632,7 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.no_title"><code class="docutils literal notranslate"><span class="pre">QueryOptions.no_title</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_burst"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_burst</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_cloudasset"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_cloudasset</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_edited"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_edited</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_favorite"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_favorite</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_hdr"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_hdr</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.not_hidden"><code class="docutils literal notranslate"><span class="pre">QueryOptions.not_hidden</span></code></a></li>
|
||||
@@ -656,7 +668,10 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.QueryOptions.year"><code class="docutils literal notranslate"><span class="pre">QueryOptions.year</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo"><code class="docutils literal notranslate"><span class="pre">ScoreInfo</span></code></a></li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo"><code class="docutils literal notranslate"><span class="pre">ScoreInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.ScoreInfo.asdict"><code class="docutils literal notranslate"><span class="pre">ScoreInfo.asdict()</span></code></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="toctree-l2"><a class="reference internal" href="reference.html#osxphotos.SearchInfo"><code class="docutils literal notranslate"><span class="pre">SearchInfo</span></code></a><ul>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.activities"><code class="docutils literal notranslate"><span class="pre">SearchInfo.activities</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.all"><code class="docutils literal notranslate"><span class="pre">SearchInfo.all</span></code></a></li>
|
||||
@@ -674,9 +689,11 @@
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.neighborhoods"><code class="docutils literal notranslate"><span class="pre">SearchInfo.neighborhoods</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.place_names"><code class="docutils literal notranslate"><span class="pre">SearchInfo.place_names</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.season"><code class="docutils literal notranslate"><span class="pre">SearchInfo.season</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.source"><code class="docutils literal notranslate"><span class="pre">SearchInfo.source</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.state"><code class="docutils literal notranslate"><span class="pre">SearchInfo.state</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.state_abbreviation"><code class="docutils literal notranslate"><span class="pre">SearchInfo.state_abbreviation</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.streets"><code class="docutils literal notranslate"><span class="pre">SearchInfo.streets</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.text_found"><code class="docutils literal notranslate"><span class="pre">SearchInfo.text_found</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.venue_types"><code class="docutils literal notranslate"><span class="pre">SearchInfo.venue_types</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.venues"><code class="docutils literal notranslate"><span class="pre">SearchInfo.venues</span></code></a></li>
|
||||
<li class="toctree-l3"><a class="reference internal" href="reference.html#osxphotos.SearchInfo.year"><code class="docutils literal notranslate"><span class="pre">SearchInfo.year</span></code></a></li>
|
||||
|
||||
BIN
docs/objects.inv
BIN
docs/objects.inv
Binary file not shown.
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Tutorial" href="tutorial.html" /><link rel="prev" title="Welcome to OSXPhotos’s documentation!" href="index.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos - osxphotos 0.54.4 documentation</title>
|
||||
<title>OSXPhotos - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -161,7 +161,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<head><meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><meta name="generator" content="Docutils 0.19: https://docutils.sourceforge.io/" />
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos python API" href="reference.html" /><link rel="prev" title="OSXPhotos Template System" href="template_help.html" />
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Python Reference" href="reference.html" /><link rel="prev" title="OSXPhotos Template System" href="template_help.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Python Package Overview - osxphotos 0.54.4 documentation</title>
|
||||
<title>OSXPhotos Python Package Overview - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -161,7 +161,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -392,7 +392,7 @@ as well as <code class="docutils literal notranslate"><span class="pre">{functio
|
||||
<div class="context">
|
||||
<span>Next</span>
|
||||
</div>
|
||||
<div class="title">OSXPhotos python API</div>
|
||||
<div class="title">OSXPhotos Python Reference</div>
|
||||
</div>
|
||||
<svg class="furo-related-icon"><use href="#svg-arrow-right"></use></svg>
|
||||
</a>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Python Module Index - osxphotos 0.54.4 documentation</title>
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Python Module Index - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -122,7 +122,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -145,7 +145,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -159,7 +159,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<meta name="color-scheme" content="light dark"><link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="#" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Search - osxphotos 0.54.4 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/><title>Search - osxphotos 0.57.3 documentation</title><link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo-extensions.css?digest=30d1aed668e5c3a91c3e3bf6a60b675221979f0e" />
|
||||
@@ -121,7 +121,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -144,7 +144,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="#" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -158,7 +158,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Python Package Overview" href="package_overview.html" /><link rel="prev" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Template System - osxphotos 0.54.4 documentation</title>
|
||||
<title>OSXPhotos Template System - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -161,7 +161,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1 current current-page"><a class="current reference internal" href="#">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
@@ -551,55 +551,58 @@
|
||||
<tr class="row-even"><td><p>{id}</p></td>
|
||||
<td><p>A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3…etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use ‘{id:05d}’ which results in 00001, 00002, 00003…etc.</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{album_seq}</p></td>
|
||||
<tr class="row-odd"><td><p>{counter}</p></td>
|
||||
<td><p>A sequential counter, starting at 0, that increments each time it is evaluated.To start counting at a value other than 0, append append ‘(starting_value)’ to the field name.For example, to start counting at 1 instead of 0: ‘{counter(1)}’.May be formatted using a python string format code.For example, to format as a 5-digit integer and pad with zeros, use ‘{counter:05d(1)}’which results in 00001, 00002, 00003…etc.You may also specify a stop value which causes the counter to reset to the starting valuewhen the stop value is reached and a step size which causes the counter to increment bythe specified value instead of 1. Use the format ‘{counter(start,stop,step)}’ where start,stop, and step are integers. For example, to count from 1 to 10 by 2, use ‘{counter(1,11,2)}’.Note that the counter stops counting when the stop value is reached and does not return thestop value. Start, stop, and step are optional and may be omitted. For example, to countfrom 0 by 2s, use ‘{counter(,,2)}’.You may create an arbitrary number of counters by appending a unique name to the field namepreceded by a period: ‘{counter.a}’, ‘{counter.b}’, etc. Each counter will have its own stateand will start at 0 and increment by 1 unless otherwise specified. Note: {counter} is not suitable for use with ‘export’ and ‘–update’ as the counter associated with a photo may change between export sessions. See also {id}.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{album_seq}</p></td>
|
||||
<td><p>An integer, starting at 0, indicating the photo’s index (sequence) in the containing album. Only valid when used in a ‘–filename’ template and only when ‘{album}’ or ‘{folder_album}’ is used in the ‘–directory’ template. For example ‘–directory “{folder_album}” –filename “{album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append append ‘(starting_value)’ to the field name. For example, to start counting at 1 instead of 0: ‘{album_seq(1)}’. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use ‘{album_seq:05d}’ which results in 00000, 00001, 00002…etc. To format while also using a starting value: ‘{album_seq:05d(1)}’ which results in 0001, 00002…etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also ‘{folder_album_seq}’.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{folder_album_seq}</p></td>
|
||||
<tr class="row-odd"><td><p>{folder_album_seq}</p></td>
|
||||
<td><p>An integer, starting at 0, indicating the photo’s index (sequence) in the containing album and folder path. Only valid when used in a ‘–filename’ template and only when ‘{folder_album}’ is used in the ‘–directory’ template. For example ‘–directory “{folder_album}” –filename “{folder_album<em>seq}</em>{original_name}”’. To start counting at a value other than 0, append ‘(starting_value)’ to the field name. For example, to start counting at 1 instead of 0: ‘{folder_album_seq(1)}’ May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use ‘{folder_album_seq:05d}’ which results in 00000, 00001, 00002…etc. To format while also using a starting value: ‘{folder_album_seq:05d(1)}’ which results in 0001, 00002…etc.This may result in incorrect sequences if you have duplicate albums with the same name in the same folder; see also ‘{album_seq}’.</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{comma}</p></td>
|
||||
<tr class="row-even"><td><p>{comma}</p></td>
|
||||
<td><p>A comma: ‘,’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{semicolon}</p></td>
|
||||
<tr class="row-odd"><td><p>{semicolon}</p></td>
|
||||
<td><p>A semicolon: ‘;’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{questionmark}</p></td>
|
||||
<tr class="row-even"><td><p>{questionmark}</p></td>
|
||||
<td><p>A question mark: ‘?’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{pipe}</p></td>
|
||||
<tr class="row-odd"><td><p>{pipe}</p></td>
|
||||
<td><p>A vertical pipe: ‘|’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{openbrace}</p></td>
|
||||
<tr class="row-even"><td><p>{openbrace}</p></td>
|
||||
<td><p>An open brace: ‘{’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{closebrace}</p></td>
|
||||
<tr class="row-odd"><td><p>{closebrace}</p></td>
|
||||
<td><p>A close brace: ‘}’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{openparens}</p></td>
|
||||
<tr class="row-even"><td><p>{openparens}</p></td>
|
||||
<td><p>An open parentheses: ‘(’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{closeparens}</p></td>
|
||||
<tr class="row-odd"><td><p>{closeparens}</p></td>
|
||||
<td><p>A close parentheses: ‘)’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{openbracket}</p></td>
|
||||
<tr class="row-even"><td><p>{openbracket}</p></td>
|
||||
<td><p>An open bracket: ‘[’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{closebracket}</p></td>
|
||||
<tr class="row-odd"><td><p>{closebracket}</p></td>
|
||||
<td><p>A close bracket: ‘]’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{newline}</p></td>
|
||||
<tr class="row-even"><td><p>{newline}</p></td>
|
||||
<td><p>A newline: ‘n’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{lf}</p></td>
|
||||
<tr class="row-odd"><td><p>{lf}</p></td>
|
||||
<td><p>A line feed: ‘n’, alias for {newline}</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{cr}</p></td>
|
||||
<tr class="row-even"><td><p>{cr}</p></td>
|
||||
<td><p>A carriage return: ‘r’</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{crlf}</p></td>
|
||||
<tr class="row-odd"><td><p>{crlf}</p></td>
|
||||
<td><p>A carriage return + line feed: ‘rn’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{tab}</p></td>
|
||||
<tr class="row-even"><td><p>{tab}</p></td>
|
||||
<td><dl class="field-list simple">
|
||||
<dt class="field-odd">A tab<span class="colon">:</span></dt>
|
||||
<dd class="field-odd"><p>‘t’</p>
|
||||
@@ -607,73 +610,73 @@
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{osxphotos_version}</p></td>
|
||||
<td><p>The osxphotos version, e.g. ‘0.54.4’</p></td>
|
||||
<tr class="row-odd"><td><p>{osxphotos_version}</p></td>
|
||||
<td><p>The osxphotos version, e.g. ‘0.57.3’</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{osxphotos_cmd_line}</p></td>
|
||||
<tr class="row-even"><td><p>{osxphotos_cmd_line}</p></td>
|
||||
<td><p>The full command line used to run osxphotos</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{album}</p></td>
|
||||
<tr class="row-odd"><td><p>{album}</p></td>
|
||||
<td><p>Album(s) photo is contained in</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{folder_album}</p></td>
|
||||
<tr class="row-even"><td><p>{folder_album}</p></td>
|
||||
<td><p>Folder path + album photo is contained in. e.g. ‘Folder/Subfolder/Album’ or just ‘Album’ if no enclosing folder</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{project}</p></td>
|
||||
<tr class="row-odd"><td><p>{project}</p></td>
|
||||
<td><p>Project(s) photo is contained in (such as greeting cards, calendars, slideshows)</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{album_project}</p></td>
|
||||
<tr class="row-even"><td><p>{album_project}</p></td>
|
||||
<td><p>Album(s) and project(s) photo is contained in; treats projects as regular albums</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{folder_album_project}</p></td>
|
||||
<tr class="row-odd"><td><p>{folder_album_project}</p></td>
|
||||
<td><p>Folder path + album (includes projects as albums) photo is contained in. e.g. ‘Folder/Subfolder/Album’ or just ‘Album’ if no enclosing folder</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{keyword}</p></td>
|
||||
<tr class="row-even"><td><p>{keyword}</p></td>
|
||||
<td><p>Keyword(s) assigned to photo</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{person}</p></td>
|
||||
<tr class="row-odd"><td><p>{person}</p></td>
|
||||
<td><p>Person(s) / face(s) in a photo</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{label}</p></td>
|
||||
<tr class="row-even"><td><p>{label}</p></td>
|
||||
<td><p>Image categorization label associated with a photo (Photos 5+ only). Labels are added automatically by Photos using machine learning algorithms to categorize images. These are not the same as {keyword} which refers to the user-defined keywords/tags applied in Photos.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{label_normalized}</p></td>
|
||||
<tr class="row-odd"><td><p>{label_normalized}</p></td>
|
||||
<td><p>All lower case version of ‘label’ (Photos 5+ only)</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{comment}</p></td>
|
||||
<tr class="row-even"><td><p>{comment}</p></td>
|
||||
<td><p>Comment(s) on shared Photos; format is ‘Person name: comment text’ (Photos 5+ only)</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{exiftool}</p></td>
|
||||
<tr class="row-odd"><td><p>{exiftool}</p></td>
|
||||
<td><p>Format: ‘{exiftool:GROUP:TAGNAME}’; use exiftool (https://exiftool.org) to extract metadata, in form GROUP:TAGNAME, from image. E.g. ‘{exiftool:EXIF:Make}’ to get camera make, or {exiftool:IPTC:Keywords} to extract keywords. See https://exiftool.org/TagNames/ for list of valid tag names. You must specify group (e.g. EXIF, IPTC, etc) as used in <code class="docutils literal notranslate"><span class="pre">exiftool</span> <span class="pre">-G</span></code>. exiftool must be installed in the path to use this template.</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{searchinfo.holiday}</p></td>
|
||||
<tr class="row-even"><td><p>{searchinfo.holiday}</p></td>
|
||||
<td><p>Holiday names associated with a photo, e.g. ‘Christmas Day’; (Photos 5+ only, applied automatically by Photos’ image categorization algorithms).</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{searchinfo.activity}</p></td>
|
||||
<tr class="row-odd"><td><p>{searchinfo.activity}</p></td>
|
||||
<td><p>Activities associated with a photo, e.g. ‘Sporting Event’; (Photos 5+ only, applied automatically by Photos’ image categorization algorithms).</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{searchinfo.venue}</p></td>
|
||||
<tr class="row-even"><td><p>{searchinfo.venue}</p></td>
|
||||
<td><p>Venues associated with a photo, e.g. name of restaurant; (Photos 5+ only, applied automatically by Photos’ image categorization algorithms).</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{searchinfo.venue_type}</p></td>
|
||||
<tr class="row-odd"><td><p>{searchinfo.venue_type}</p></td>
|
||||
<td><p>Venue types associated with a photo, e.g. ‘Restaurant’; (Photos 5+ only, applied automatically by Photos’ image categorization algorithms).</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{photo}</p></td>
|
||||
<tr class="row-even"><td><p>{photo}</p></td>
|
||||
<td><p>Provides direct access to the PhotoInfo object for the photo. Must be used in format ‘{photo.property}’ where ‘property’ represents a PhotoInfo property. For example: ‘{photo.favorite}’ is the same as ‘{favorite}’ and ‘{photo.place.name}’ is the same as ‘{place.name}’. ‘{photo}’ provides access to properties that are not available as separate template fields but it assumes some knowledge of the underlying PhotoInfo class. See <a class="reference external" href="https://rhettbull.github.io/osxphotos/">https://rhettbull.github.io/osxphotos/</a> for additional documentation on the PhotoInfo class.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{detected_text}</p></td>
|
||||
<tr class="row-odd"><td><p>{detected_text}</p></td>
|
||||
<td><p>List of text strings found in the image after performing text detection. Using ‘{detected_text}’ will cause osxphotos to perform text detection on your photos using the built-in macOS text detection algorithms which will slow down your export. The results for each photo will be cached in the export database so that future exports with ‘–update’ do not need to reprocess each photo. You may pass a confidence threshold value between 0.0 and 1.0 after a colon as in ‘{detected_text:0.5}’; The default confidence threshold is 0.75. ‘{detected_text}’ works only on macOS Catalina (10.15) or later. Note: this feature is not the same thing as Live Text in macOS Monterey, which osxphotos does not yet support.</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{shell_quote}</p></td>
|
||||
<tr class="row-even"><td><p>{shell_quote}</p></td>
|
||||
<td><p>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.</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{strip}</p></td>
|
||||
<tr class="row-odd"><td><p>{strip}</p></td>
|
||||
<td><p>Use in form ‘{strip,TEMPLATE}’; strips whitespace from begining and end of rendered TEMPLATE value(s).</p></td>
|
||||
</tr>
|
||||
<tr class="row-odd"><td><p>{format}</p></td>
|
||||
<tr class="row-even"><td><p>{format}</p></td>
|
||||
<td><p>Use in form, ‘{format:TYPE:FORMAT,TEMPLATE}’; converts TEMPLATE value to TYPE then formats the value using Python string formatting codes specified by FORMAT; TYPE is one of: ‘int’, ‘float’, or ‘str’. For example, ‘{format:float:.1f,{exiftool:EXIF:FocalLength}}’ will format focal length to 1 decimal place (e.g. ‘100.0’).</p></td>
|
||||
</tr>
|
||||
<tr class="row-even"><td><p>{function}</p></td>
|
||||
<tr class="row-odd"><td><p>{function}</p></td>
|
||||
<td><p>Execute a python function from an external file and use return value as template substitution. Use in format: {function:file.py::function_name} where ‘file.py’ is the name of the python file and ‘function_name’ is the name of the function to call. The function will be passed the PhotoInfo object for the photo. See https://github.com/RhetTbull/osxphotos/blob/master/examples/template_function.py for an example of how to implement a template function.</p></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<link rel="index" title="Index" href="genindex.html" /><link rel="search" title="Search" href="search.html" /><link rel="next" title="OSXPhotos Command Line Interface (CLI)" href="cli.html" /><link rel="prev" title="OSXPhotos" href="overview.html" />
|
||||
|
||||
<meta name="generator" content="sphinx-5.3.0, furo 2022.09.29"/>
|
||||
<title>OSXPhotos Tutorial - osxphotos 0.54.4 documentation</title>
|
||||
<title>OSXPhotos Tutorial - osxphotos 0.57.3 documentation</title>
|
||||
<link rel="stylesheet" type="text/css" href="_static/pygments.css" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/styles/furo.css?digest=d81277517bee4d6b0349d71bb2661d4890b5617c" />
|
||||
<link rel="stylesheet" type="text/css" href="_static/copybutton.css" />
|
||||
@@ -124,7 +124,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div class="header-center">
|
||||
<a href="index.html"><div class="brand">osxphotos 0.54.4 documentation</div></a>
|
||||
<a href="index.html"><div class="brand">osxphotos 0.57.3 documentation</div></a>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-toggle-container theme-toggle-header">
|
||||
@@ -147,7 +147,7 @@
|
||||
<div class="sidebar-sticky"><a class="sidebar-brand" href="index.html">
|
||||
|
||||
|
||||
<span class="sidebar-brand-text">osxphotos 0.54.4 documentation</span>
|
||||
<span class="sidebar-brand-text">osxphotos 0.57.3 documentation</span>
|
||||
|
||||
</a><form class="sidebar-search-container" method="get" action="search.html" role="search">
|
||||
<input class="sidebar-search" placeholder=Search name="q" aria-label="Search">
|
||||
@@ -161,7 +161,7 @@
|
||||
<li class="toctree-l1"><a class="reference internal" href="cli.html">OSXPhotos Command Line Interface (CLI)</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="template_help.html">OSXPhotos Template System</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="package_overview.html">OSXPhotos Python Package Overview</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos python API</a></li>
|
||||
<li class="toctree-l1"><a class="reference internal" href="reference.html">OSXPhotos Python Reference</a></li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
OSXPhotos python API
|
||||
====================
|
||||
OSXPhotos Python Reference
|
||||
==========================
|
||||
|
||||
.. automodule:: osxphotos
|
||||
:members:
|
||||
@@ -322,6 +322,8 @@ Template Substitutions
|
||||
- A shorter representation of photo's internal universally unique identifier (UUID) for the photo, a 22-character string unique to the photo, e.g. 'JYsxugP9UjetmCbBCHXcmu'
|
||||
* - {id}
|
||||
- A unique number for the photo based on its primary key in the Photos database. A sequential integer, e.g. 1, 2, 3...etc. Each asset associated with a photo (e.g. an image and Live Photo preview) will share the same id. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{id:05d}' which results in 00001, 00002, 00003...etc.
|
||||
* - {counter}
|
||||
- A sequential counter, starting at 0, that increments each time it is evaluated.To start counting at a value other than 0, append append '(starting_value)' to the field name.For example, to start counting at 1 instead of 0: '{counter(1)}'.May be formatted using a python string format code.For example, to format as a 5-digit integer and pad with zeros, use '{counter:05d(1)}'which results in 00001, 00002, 00003...etc.You may also specify a stop value which causes the counter to reset to the starting valuewhen the stop value is reached and a step size which causes the counter to increment bythe specified value instead of 1. Use the format '{counter(start,stop,step)}' where start,stop, and step are integers. For example, to count from 1 to 10 by 2, use '{counter(1,11,2)}'.Note that the counter stops counting when the stop value is reached and does not return thestop value. Start, stop, and step are optional and may be omitted. For example, to countfrom 0 by 2s, use '{counter(,,2)}'.You may create an arbitrary number of counters by appending a unique name to the field namepreceded by a period: '{counter.a}', '{counter.b}', etc. Each counter will have its own stateand will start at 0 and increment by 1 unless otherwise specified. Note: {counter} is not suitable for use with 'export' and '--update' as the counter associated with a photo may change between export sessions. See also {id}.
|
||||
* - {album_seq}
|
||||
- An integer, starting at 0, indicating the photo's index (sequence) in the containing album. Only valid when used in a '--filename' template and only when '{album}' or '{folder_album}' is used in the '--directory' template. For example '--directory "{folder_album}" --filename "{album\ *seq}*\ {original_name}"'. To start counting at a value other than 0, append append '(starting_value)' to the field name. For example, to start counting at 1 instead of 0: '{album_seq(1)}'. May be formatted using a python string format code. For example, to format as a 5-digit integer and pad with zeros, use '{album_seq:05d}' which results in 00000, 00001, 00002...etc. To format while also using a starting value: '{album_seq:05d(1)}' which results in 0001, 00002...etc.This may result in incorrect sequences if you have duplicate albums with the same name; see also '{folder_album_seq}'.
|
||||
* - {folder_album_seq}
|
||||
@@ -357,7 +359,7 @@ Template Substitutions
|
||||
* - {tab}
|
||||
- :A tab: '\t'
|
||||
* - {osxphotos_version}
|
||||
- The osxphotos version, e.g. '0.54.4'
|
||||
- The osxphotos version, e.g. '0.57.3'
|
||||
* - {osxphotos_cmd_line}
|
||||
- The full command line used to run osxphotos
|
||||
* - {album}
|
||||
|
||||
67
examples/cli_example_1.py
Normal file
67
examples/cli_example_1.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @query_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_1.py` or `python cli_example_1.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add all the query options available in `osxphotos query` as command line options
|
||||
as well as the following options:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will perform the query and pass the list of filtered PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
|
||||
For example, to run the command on only selected photos:
|
||||
|
||||
osxphotos run cli_example_1.py --selected
|
||||
|
||||
To run the command on all photos with the keyword "foo":
|
||||
|
||||
osxphotos run cli_example_1.py --keyword foo
|
||||
|
||||
For more advanced example, see `cli_example_2.py`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import query_command, verbose
|
||||
|
||||
|
||||
@query_command
|
||||
def example(photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample query command for osxphotos. Prints out the filename and date of each photo.
|
||||
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_1.py --help` or `python cli_example_1.py --help`
|
||||
"""
|
||||
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found {len(photos)} photo(s)")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
verbose(f"Processing {photo.original_filename}")
|
||||
print(f"{photo.original_filename} {photo.date}")
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
160
examples/cli_example_2.py
Normal file
160
examples/cli_example_2.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @query_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_2.py` or `python cli_example_2.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add all the query options available in `osxphotos query` as command line options
|
||||
as well as the following options:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will perform the query and pass the list of filtered PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
|
||||
For example, to run the command on only selected photos:
|
||||
|
||||
osxphotos run cli_example_2.py --selected
|
||||
|
||||
To run the command on all photos with the keyword "foo":
|
||||
|
||||
osxphotos run cli_example_2.py --keyword foo
|
||||
|
||||
The following helper functions may be useful and can be imported from osxphotos.cli:
|
||||
|
||||
abort(message: str, exit_code: int = 1)
|
||||
Abort with error message and exit code
|
||||
echo(message: str)
|
||||
Print message to stdout using rich formatting
|
||||
echo_error(message: str)
|
||||
Print message to stderr using rich formatting
|
||||
logger: logging.Logger
|
||||
Python logger for osxphotos; for example, logger.debug("debug message")
|
||||
verbose(*args, level: int = 1)
|
||||
Print args to stdout if --verbose option is set
|
||||
query_command: decorator to create an osxphotos query command
|
||||
kvstore(name: str) -> SQLiteKVStore useful for storing state between runs
|
||||
|
||||
The verbose, echo, and echo_error functions use rich formatting to print messages to stdout and stderr.
|
||||
See https://github.com/Textualize/rich for more information on rich formatting.
|
||||
|
||||
In addition to standard rich formatting styles, the following styles will be defined
|
||||
(and can be changed using --theme):
|
||||
|
||||
[change]: something change
|
||||
[no_change]: indicate no change
|
||||
[count]: a count
|
||||
[error]: an error
|
||||
[filename]: a filename
|
||||
[filepath]: a filepath
|
||||
[num]: a number
|
||||
[time]: a time or date
|
||||
[tz]: a timezone
|
||||
[warning]: a warning
|
||||
[uuid]: a uuid
|
||||
|
||||
The tags should be closed with [/] to end the style. For example:
|
||||
|
||||
echo("[filename]foo[/] [time]bar[/]")
|
||||
|
||||
For simpler examples, see `cli_example_1.py`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import click
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import (
|
||||
abort,
|
||||
echo,
|
||||
echo_error,
|
||||
kvstore,
|
||||
logger,
|
||||
query_command,
|
||||
verbose,
|
||||
)
|
||||
|
||||
|
||||
@query_command()
|
||||
@click.option(
|
||||
"--resume",
|
||||
is_flag=True,
|
||||
help="Resume processing from last run, do not reprocess photos",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run", is_flag=True, help="Do a dry run, don't actually do anything"
|
||||
)
|
||||
def example(resume, dry_run, photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample query command for osxphotos. Prints out the filename and date of each photo.
|
||||
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_2.py --help` or `python cli_example_2.py --help`
|
||||
|
||||
The @query_command decorator returns a click.command so you can add additional options
|
||||
using standard click decorators. For example, the --resume and --dry-run options.
|
||||
For more information on click, see https://palletsprojects.com/p/click/.
|
||||
"""
|
||||
|
||||
# abort will print the message to stderr and exit with the given exit code
|
||||
if not photos:
|
||||
abort("Nothing to do!", 1)
|
||||
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found [count]{len(photos)}[/] photos")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# the logger is a python logging.Logger object
|
||||
# debug messages will only be printed if --debug option is set
|
||||
logger.debug(f"{kwargs=}")
|
||||
|
||||
# kvstore() returns a SQLiteKVStore object for storing state between runs
|
||||
# this is basically a persistent dictionary that can be used to store state
|
||||
# see https://github.com/RhetTbull/sqlitekvstore for more information
|
||||
kv = kvstore("cli_example_2")
|
||||
verbose(f"Using key-value cache: {kv.path}")
|
||||
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
if resume and photo.uuid in kv:
|
||||
echo(
|
||||
f"Skipping processed photo [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
continue
|
||||
|
||||
# store the uuid and current time in the kvstore
|
||||
# the key and value must be a type supported by SQLite: int, float, str, bytes, bool, None
|
||||
# if you need to store other values, you should serialize them to a string or bytes first
|
||||
# for example, using json.dumps() or pickle.dumps()
|
||||
kv[photo.uuid] = datetime.datetime.now().isoformat()
|
||||
echo(f"Processing [filename]{photo.original_filename}[/] [time]{photo.date}[/]")
|
||||
if not dry_run:
|
||||
# do something with the photo here
|
||||
echo(f"Doing something with [filename]{photo.original_filename}[/]")
|
||||
|
||||
# echo_error will print to stderr
|
||||
# if you add [warning] or [error], it will be formatted accordingly
|
||||
# and include an emoji to make the message stand out
|
||||
echo_error("[warning]This is a warning message!")
|
||||
echo_error("[error]This is an error message!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
60
examples/cli_example_3.py
Normal file
60
examples/cli_example_3.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Sample query command for osxphotos
|
||||
|
||||
This shows how simple it is to create a command line tool using osxphotos to process your photos.
|
||||
|
||||
Using the @selection_command decorator turns your function to a full-fledged command line app that
|
||||
can be run via `osxphotos run cli_example_1.py` or `python cli_example_1.py` if you have pip installed osxphotos.
|
||||
|
||||
Using this decorator makes it very easy to create a quick command line tool that can operate on
|
||||
a subset of your photos. Additionally, writing a command in this way makes it easy to later
|
||||
incorporate the command into osxphotos as a full-fledged command.
|
||||
|
||||
The decorator will add the following options to your command:
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
--debug (hidden, won't show in help)
|
||||
|
||||
The decorated function will get the selected photos and pass the list of PhotoInfo objects
|
||||
to your function. You can then do whatever you want with the photos.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.cli import (
|
||||
selection_command,
|
||||
verbose,
|
||||
)
|
||||
|
||||
|
||||
@selection_command
|
||||
def example(photos: list[osxphotos.PhotoInfo], **kwargs):
|
||||
"""Sample command for osxphotos. Prints out the filename and date of each photo
|
||||
currently selected in Photos.app.
|
||||
|
||||
Whatever text you put in the function's docstring here, will be used as the command's
|
||||
help text when run via `osxphotos run cli_example_1.py --help` or `python cli_example_1.py --help`
|
||||
"""
|
||||
|
||||
# verbose() will print to stdout if --verbose option is set
|
||||
# you can optionally provide a level (default is 1) to print only if --verbose is set to that level
|
||||
# for example: -VV or --verbose --verbose == level 2
|
||||
verbose(f"Found {len(photos)} photo(s)")
|
||||
verbose("This message will only be printed if verbose level 2 is set", level=2)
|
||||
|
||||
# do something with photos here
|
||||
for photo in photos:
|
||||
# photos is a list of PhotoInfo objects
|
||||
# see: https://rhettbull.github.io/osxphotos/reference.html#osxphotos.PhotoInfo
|
||||
verbose(f"Processing {photo.original_filename}")
|
||||
print(f"{photo.original_filename} {photo.date}")
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# call your function here
|
||||
# you do not need to pass any arguments to the function
|
||||
# as the decorator will handle parsing the command line arguments
|
||||
example()
|
||||
151
examples/find_bad_extensions.py
Normal file
151
examples/find_bad_extensions.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||
|
||||
This can be run with osxphotos via: osxphotos run find_bad_extensions.py
|
||||
|
||||
For help, run: osxphotos run find_bad_extensions.py --help
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import click
|
||||
from rich import print
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
from osxphotos.cli.common import get_data_dir
|
||||
from osxphotos.exiftool import ExifTool, get_exiftool_path
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
|
||||
def check_extension(filepath: str) -> tuple[bool, str, str]:
|
||||
"""Check if file extension is correct for image file using exiftool
|
||||
|
||||
Args:
|
||||
filepath: path to file to check
|
||||
|
||||
Returns: tuple of (bool, str, str) where bool is True if extension is correct, False if not
|
||||
and str, str is the current extension, correct extension or current extension if correct
|
||||
"""
|
||||
filepath = pathlib.Path(filepath)
|
||||
current_ext = filepath.suffix.lower()
|
||||
|
||||
current_ext = current_ext[1:] if current_ext else "" # remove leading dot
|
||||
exiftool = ExifTool(filepath)
|
||||
correct_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||
if current_ext != correct_ext:
|
||||
# there are some extensions that have more than one valid extension
|
||||
# there are likely more but these are the ones I've seen so far
|
||||
is_correct = (
|
||||
current_ext in ("jpg", "jpeg") and correct_ext in ("jpg", "jpeg")
|
||||
) or (current_ext in ("tif", "tiff") and correct_ext in ("tif", "tiff"))
|
||||
else:
|
||||
is_correct = True
|
||||
return is_correct, current_ext, correct_ext
|
||||
|
||||
|
||||
def check_photo(
|
||||
photo: PhotoInfo, recheck: bool, version: str, kvstore: SQLiteKVStore
|
||||
) -> None:
|
||||
"""Check PhotoInfo for correct extension
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance
|
||||
recheck: if True, recheck even if previously checked
|
||||
version: "original" or "edited"
|
||||
kvstore: SQLiteKVStore instance to store results
|
||||
"""
|
||||
photo_path = photo.path if version == "original" else photo.path_edited
|
||||
if photo_path is None:
|
||||
print(
|
||||
f":warning-emoji: [yellow]No {version} path for photo: {photo.original_filename} ({photo.uuid})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
if recheck or f"{photo.uuid}:{version}" not in kvstore:
|
||||
is_correct, current_ext, correct_ext = check_extension(photo_path)
|
||||
if not is_correct:
|
||||
print(
|
||||
f"{photo.original_filename} ({version}) has incorrect extension: [red]{current_ext}[/] should be [green]{correct_ext}[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
# output results as CSV to stdout
|
||||
csv.writer(sys.stdout).writerow(
|
||||
[
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
version,
|
||||
current_ext,
|
||||
correct_ext,
|
||||
photo_path,
|
||||
]
|
||||
)
|
||||
kvstore[f"{photo.uuid}:{version}"] = (is_correct, current_ext, correct_ext)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--library",
|
||||
default=None,
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
||||
help="Path to Photos library to use. Default is to use default Photos library.",
|
||||
)
|
||||
@click.option(
|
||||
"--recheck",
|
||||
is_flag=True,
|
||||
help="Recheck all files even if previously checked and cached.",
|
||||
)
|
||||
@click.option(
|
||||
"--edited",
|
||||
is_flag=True,
|
||||
help="Check edited versions of photos in addition to originals.",
|
||||
)
|
||||
def main(library: str, recheck: bool, edited: bool):
|
||||
"""Scan Photos library to find photos with bad (incorrect) file extensions.
|
||||
|
||||
This can be run with osxphotos via: `osxphotos run find_bad_extensions.py`
|
||||
|
||||
Both STDOUT and STDERR are used to output results.
|
||||
|
||||
STDOUT is used to output a CSV file with the following columns:
|
||||
|
||||
uuid, original_filename, version, current_extension, correct_extension, path
|
||||
|
||||
Thus, to save the results to a file, run:
|
||||
|
||||
osxphotos run find_bad_extensions.py > results.csv
|
||||
"""
|
||||
|
||||
# exiftool required to run
|
||||
try:
|
||||
get_exiftool_path()
|
||||
except FileNotFoundError as e:
|
||||
print(
|
||||
":cross_mark-emoji: [red]Could not find exiftool. Please download and install"
|
||||
" from https://exiftool.org/",
|
||||
file=sys.stderr,
|
||||
)
|
||||
raise click.Abort() from e
|
||||
|
||||
# path to the cache database to store results of extension check
|
||||
cache_db_path = os.path.join(get_data_dir(), "bad_extensions.db")
|
||||
kvstore = SQLiteKVStore(
|
||||
cache_db_path, wal=True, serialize=json.dumps, deserialize=json.loads
|
||||
)
|
||||
kvstore.about = "osxphotos bad extensions cache"
|
||||
print(f"Using cache database: [blue]{cache_db_path}", file=sys.stderr)
|
||||
|
||||
# load the Photos database and check each photo
|
||||
photosdb = PhotosDB(dbfile=library)
|
||||
for photo in photosdb.photos():
|
||||
check_photo(photo, recheck, "original", kvstore)
|
||||
if edited and photo.hasadjustments:
|
||||
check_photo(photo, recheck, "edited", kvstore)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
44
examples/fix_export_extension.py
Normal file
44
examples/fix_export_extension.py
Normal file
@@ -0,0 +1,44 @@
|
||||
""" Example function for use with osxphotos export --post-function option """
|
||||
|
||||
import pathlib
|
||||
from typing import Callable
|
||||
|
||||
from osxphotos import ExportResults, PhotoInfo
|
||||
from osxphotos.exiftool import ExifTool
|
||||
|
||||
|
||||
def fix_extension(
|
||||
photo: PhotoInfo, results: ExportResults, verbose: Callable, **kwargs
|
||||
):
|
||||
"""Call this with osxphotos export /path/to/export --post-function fix_export_extension.py::fix_extension
|
||||
This will get called immediately after the photo has been exported
|
||||
|
||||
See full example here: https://github.com/RhetTbull/osxphotos/blob/master/examples/post_function.py
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo instance for the photo that's just been exported
|
||||
results: ExportResults instance with information about the files associated with the exported photo
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
Use verbose(str) instead of print if you want your function to conditionally output text depending on --verbose flag
|
||||
Any string printed with verbose that contains "warning" or "error" (case-insensitive) will be printed with the appropriate warning or error color
|
||||
Will not be called if --dry-run flag is enabled
|
||||
Will be called immediately after export and before any --post-command commands are executed
|
||||
"""
|
||||
|
||||
for filepath in results.exported:
|
||||
filepath = pathlib.Path(filepath)
|
||||
ext = filepath.suffix.lower()
|
||||
if not ext:
|
||||
continue
|
||||
ext = ext[1:] # remove leading dot
|
||||
exiftool = ExifTool(filepath)
|
||||
actual_ext = exiftool.asdict().get("File:FileTypeExtension").lower()
|
||||
if ext != actual_ext and (ext not in ("jpg", "jpeg") or actual_ext != "jpg"):
|
||||
# WARNING: Does not check for name collisions; left as an exercise for the reader
|
||||
verbose(f"Fixing extension for {filepath} from {ext} to {actual_ext}")
|
||||
new_filepath = filepath.with_suffix(f".{actual_ext}")
|
||||
verbose(f"Renaming {filepath} to {new_filepath}")
|
||||
filepath.rename(new_filepath)
|
||||
@@ -3,9 +3,15 @@
|
||||
import typing as t
|
||||
import photoscript
|
||||
import pathlib
|
||||
from osxphotos.cli.import_cli import ReportRecord
|
||||
|
||||
|
||||
def post_function(
|
||||
photo: photoscript.Photo, filepath: pathlib.Path, verbose: t.Callable, **kwargs
|
||||
photo: photoscript.Photo,
|
||||
filepath: pathlib.Path,
|
||||
verbose: t.Callable,
|
||||
report_record: ReportRecord,
|
||||
**kwargs,
|
||||
):
|
||||
"""Call this with osxphotos import /file/to/import --post-function post_function.py::post_function
|
||||
This will get called immediately after the photo has been imported into Photos
|
||||
@@ -15,6 +21,7 @@ def post_function(
|
||||
photo: photoscript.Photo instance for the photo that's just been imported
|
||||
filepath: pathlib.Path to the file that was imported (this is the path to the source file, not the path inside the Photos library)
|
||||
verbose: A function to print verbose output if --verbose is set; if --verbose is not set, acts as a no-op (nothing gets printed)
|
||||
report_record: ReportRecord instance for the photo that's just been imported; update this if you want to change the report output
|
||||
**kwargs: reserved for future use; recommend you include **kwargs so your function still works if additional arguments are added in future versions
|
||||
|
||||
Notes:
|
||||
@@ -25,4 +32,16 @@ def post_function(
|
||||
|
||||
# add a note to the photo's description
|
||||
verbose("Adding note to description")
|
||||
photo.description = f"{photo.description} (imported with osxphotos)"
|
||||
description = photo.description
|
||||
description = (
|
||||
f"{description} (imported with osxphotos)"
|
||||
if description
|
||||
else "(imported with osxphotos)"
|
||||
)
|
||||
|
||||
# update report_record if you modify the photo and want the report to reflect the change
|
||||
# the report_record object passed to the function is mutable so you can update it directly
|
||||
report_record.description = description
|
||||
|
||||
# update the photo's description via the Photo object
|
||||
photo.description = description
|
||||
|
||||
38
examples/simple_example.py
Normal file
38
examples/simple_example.py
Normal file
@@ -0,0 +1,38 @@
|
||||
""" Simple usage of the package """
|
||||
import os.path
|
||||
|
||||
import osxphotos
|
||||
|
||||
def main():
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.albums)
|
||||
|
||||
print(photosdb.keywords_as_dict)
|
||||
print(photosdb.persons_as_dict)
|
||||
print(photosdb.albums_as_dict)
|
||||
|
||||
# find all photos with Keyword = Foo and containing John Smith
|
||||
photos = photosdb.photos(keywords=["Foo"],persons=["John Smith"])
|
||||
|
||||
# find all photos that include Alice Smith but do not contain the keyword Bar
|
||||
photos = [p for p in photosdb.photos(persons=["Alice Smith"])
|
||||
if p not in photosdb.photos(keywords=["Bar"]) ]
|
||||
for p in photos:
|
||||
print(
|
||||
p.uuid,
|
||||
p.filename,
|
||||
p.original_filename,
|
||||
p.date,
|
||||
p.description,
|
||||
p.title,
|
||||
p.keywords,
|
||||
p.albums,
|
||||
p.persons,
|
||||
p.path,
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
84
examples/template_function_expected_path.py
Normal file
84
examples/template_function_expected_path.py
Normal file
@@ -0,0 +1,84 @@
|
||||
""" Example showing how to use a custom function for osxphotos {function} template
|
||||
Returns expected path for a missing photos
|
||||
Use: osxphotos query --missing --field original_path "{function:photopath.py::original}"
|
||||
or for edited photos: osxphotos query --missing --field edited_path "{function:photopath.py::edited}"
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from osxphotos import ExportOptions, PhotoInfo
|
||||
from osxphotos._constants import _MOVIE_TYPE, _PHOTO_TYPE, _PHOTOS_5_SHARED_PHOTO_PATH
|
||||
|
||||
|
||||
def original(
|
||||
photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs
|
||||
) -> Union[list[str], str]:
|
||||
"""returns expected path for original photo or None if path cannot be determined
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: osxphotos.ExportOptions object
|
||||
args: optional str of arguments passed to template function
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
if photo._info["shared"]:
|
||||
# shared photo
|
||||
return os.path.join(
|
||||
photo._db._library_path,
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH,
|
||||
photo._info["directory"],
|
||||
photo._info["filename"],
|
||||
)
|
||||
elif photo._info["directory"].startswith("/"):
|
||||
# referenced photo
|
||||
return os.path.join(photo._info["directory"], photo._info["filename"])
|
||||
else:
|
||||
# regular photo
|
||||
return os.path.join(
|
||||
photo._db._masters_path,
|
||||
photo._info["directory"],
|
||||
photo._info["filename"],
|
||||
)
|
||||
|
||||
|
||||
def edited(
|
||||
photo: PhotoInfo, options: ExportOptions, args: Optional[str] = None, **kwargs
|
||||
) -> Union[list[str], str]:
|
||||
"""returns expected path for edited photo or None if path cannot be determined
|
||||
|
||||
Args:
|
||||
photo: osxphotos.PhotoInfo object
|
||||
options: osxphotos.ExportOptions object
|
||||
args: optional str of arguments passed to template function
|
||||
**kwargs: not currently used, placeholder to keep functions compatible with possible changes to {function}
|
||||
|
||||
Returns:
|
||||
str or list of str of values that should be substituted for the {function} template
|
||||
"""
|
||||
|
||||
if not photo._info["hasAdjustments"]:
|
||||
return []
|
||||
|
||||
library = photo._db._library_path
|
||||
directory = photo._uuid[0] # first char of uuid
|
||||
filename = None
|
||||
if photo._info["type"] == _PHOTO_TYPE:
|
||||
# it's a photo
|
||||
if photo._db._photos_ver != 5 and photo.uti == "public.heic":
|
||||
filename = f"{photo._uuid}_1_201_a.heic"
|
||||
else:
|
||||
filename = f"{photo._uuid}_1_201_a.jpeg"
|
||||
elif photo._info["type"] == _MOVIE_TYPE:
|
||||
# it's a movie
|
||||
filename = f"{photo._uuid}_2_0_a.mov"
|
||||
else:
|
||||
return []
|
||||
|
||||
return os.path.join(library, "resources", "renders", directory, filename)
|
||||
39
examples/timewarp_filename.py
Normal file
39
examples/timewarp_filename.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""Example function for use with `osxphotos timewarp --function`
|
||||
|
||||
Call this as: `osxphotos timewarp --function timewarp_filename.py::parse_date_time_from_filename`
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Callable
|
||||
|
||||
from photoscript import Photo
|
||||
from strpdatetime import strpdatetime
|
||||
|
||||
|
||||
def parse_date_time_from_filename(
|
||||
photo: Photo, path: str | None, tz_sec: int, tz_name: str, verbose: Callable
|
||||
) -> tuple[datetime, int]:
|
||||
"""Function for use with `osxphotos timewarp --function` that parses date/time from filename in format "YYYY-MM-DD FILENAME.jpg"
|
||||
|
||||
Args:
|
||||
photo: Photo object
|
||||
path: path to photo, which may be None if photo is not on disk
|
||||
tz_sec: timezone offset from UTC in seconds
|
||||
tz_name: timezone name
|
||||
verbose: function to print verbose messages
|
||||
|
||||
Returns:
|
||||
tuple of (new date/time as datetime.datetime, and new timezone offset from UTC in seconds as int)
|
||||
"""
|
||||
filename = photo.filename
|
||||
try:
|
||||
datetime = strpdatetime(filename, "^%Y-%m-%d")
|
||||
except ValueError:
|
||||
verbose(f"Unable to parse date/time from {filename}")
|
||||
return photo.date, tz_sec
|
||||
|
||||
verbose(f"Updating {photo.filename} date/time: {datetime}")
|
||||
|
||||
return datetime, tz_sec
|
||||
@@ -24,11 +24,32 @@ datas.extend(
|
||||
]
|
||||
)
|
||||
|
||||
package_imports = [["photoscript", ["photoscript.applescript"]]]
|
||||
package_imports = [
|
||||
["photoscript", ["photoscript.applescript"]],
|
||||
]
|
||||
for package, files in package_imports:
|
||||
proot = os.path.dirname(importlib.import_module(package).__file__)
|
||||
datas.extend((os.path.join(proot, f), package) for f in files)
|
||||
|
||||
# Add attribute data files for osxmetadata
|
||||
# There is probably a better way to do this but this works
|
||||
proot = os.path.dirname(importlib.import_module("osxmetadata").__file__)
|
||||
for attribute_data in [
|
||||
"audio_attributes.json",
|
||||
"common_attributes.json",
|
||||
"filesystem_attributes.json",
|
||||
"image_attributes.json",
|
||||
"mdimporter_constants.json",
|
||||
"nsurl_resource_keys.json",
|
||||
"video_attributes.json",
|
||||
]:
|
||||
datas.append(
|
||||
(
|
||||
os.path.join(proot, "attribute_data", attribute_data),
|
||||
"osxmetadata/attribute_data",
|
||||
)
|
||||
)
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
@@ -63,4 +84,5 @@ exe = EXE(
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
target_architecture="universal2",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
"""__init__.py for osxphotos"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from ._constants import AlbumSortOrder
|
||||
@@ -20,8 +24,13 @@ from .placeinfo import PlaceInfo
|
||||
from .queryoptions import QueryOptions
|
||||
from .scoreinfo import ScoreInfo
|
||||
from .searchinfo import SearchInfo
|
||||
from .utils import _get_logger
|
||||
|
||||
# configure logging; every module in osxphotos should use this logger
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(filename)s - %(lineno)d - %(message)s",
|
||||
)
|
||||
logger: logging.Logger = logging.getLogger("osxphotos")
|
||||
if not is_debug():
|
||||
logging.disable(logging.DEBUG)
|
||||
|
||||
@@ -37,6 +46,7 @@ __all__ = [
|
||||
"ExportResults",
|
||||
"FileUtil",
|
||||
"FileUtilNoOp",
|
||||
"FolderInfo",
|
||||
"ImportInfo",
|
||||
"LikeInfo",
|
||||
"MomentInfo",
|
||||
@@ -53,8 +63,7 @@ __all__ = [
|
||||
"ScoreInfo",
|
||||
"SearchInfo",
|
||||
"__version__",
|
||||
"_get_logger",
|
||||
"is_debug",
|
||||
"logger",
|
||||
"set_debug",
|
||||
"FolderInfo",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Constants used by osxphotos
|
||||
"""
|
||||
""" Constants used by osxphotos """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os.path
|
||||
from datetime import datetime
|
||||
@@ -121,6 +121,8 @@ _TESTED_OS_VERSIONS = [
|
||||
("12", "4"),
|
||||
("12", "5"),
|
||||
("12", "6"),
|
||||
("13", "0"),
|
||||
("13", "1"),
|
||||
]
|
||||
|
||||
# Photos 5 has persons who are empty string if unidentified face
|
||||
@@ -133,6 +135,13 @@ _EXIF_TOOL_URL = "https://exiftool.org/"
|
||||
|
||||
# Where are shared iCloud photos located?
|
||||
_PHOTOS_5_SHARED_PHOTO_PATH = "resources/cloudsharing/data"
|
||||
_PHOTOS_8_SHARED_PHOTO_PATH = "scopes/cloudsharing/data"
|
||||
|
||||
# Where are shared iCloud derivatives located?
|
||||
_PHOTOS_5_SHARED_DERIVATIVE_PATH = (
|
||||
"resources/cloudsharing/resources/derivatives/masters"
|
||||
)
|
||||
_PHOTOS_8_SHARED_DERIVATIVE_PATH = "scopes/cloudsharing/resources/derivatives/masters"
|
||||
|
||||
# What type of file? Based on ZGENERICASSET.ZKIND in Photos 5 database
|
||||
_PHOTO_TYPE = 0
|
||||
@@ -171,6 +180,9 @@ _MAX_IPTC_KEYWORD_LEN = 64
|
||||
# If anyone has a keyword matching this, then too bad...
|
||||
_OSXPHOTOS_NONE_SENTINEL = "OSXPhotosXYZZY42_Sentinel$"
|
||||
|
||||
# Lock file extension for reserving filenames when exporting
|
||||
_OSXPHOTOS_LOCK_EXTENSION = ".osxphotos.lock"
|
||||
|
||||
|
||||
class SearchCategory:
|
||||
"""SearchInfo categories for Photos 5+; corresponds to categories in database/search/psi.sqlite:groups.category
|
||||
@@ -206,11 +218,11 @@ class SearchCategory:
|
||||
TITLE = 2017
|
||||
DESCRIPTION = 2018
|
||||
HOME = 2020
|
||||
WORK = 2036
|
||||
PERSON = 2021
|
||||
ACTIVITY = 2027
|
||||
HOLIDAY = 2029
|
||||
SEASON = 2030
|
||||
WORK = 2036
|
||||
VENUE = 2038
|
||||
VENUE_TYPE = 2039
|
||||
PHOTO_TYPE_VIDEO = 2044
|
||||
@@ -223,6 +235,7 @@ class SearchCategory:
|
||||
PHOTO_TYPE_PORTRAIT = 2053
|
||||
PHOTO_TYPE_SELFIES = 2054
|
||||
PHOTO_TYPE_FAVORITES = 2055
|
||||
PHOTO_TYPE_ANIMATED = None # Photos 8+ only
|
||||
MEDIA_TYPES = [
|
||||
PHOTO_TYPE_VIDEO,
|
||||
PHOTO_TYPE_SLOMO,
|
||||
@@ -236,15 +249,45 @@ class SearchCategory:
|
||||
PHOTO_TYPE_FAVORITES,
|
||||
]
|
||||
PHOTO_NAME = 2056
|
||||
CAMERA = None # Photos 8+ only
|
||||
DETECTED_TEXT = None # Photos 8+ only
|
||||
CAMERA = None # Photos 8+ only
|
||||
TEXT_FOUND = None # Photos 8+ only
|
||||
DETECTED_TEXT = None # Photos 8+ only
|
||||
SOURCE = None # Photos 8+ only
|
||||
|
||||
@classmethod
|
||||
def categories(cls) -> dict[int, str]:
|
||||
"""Return categories as dict of value: name"""
|
||||
# a bit of a hack to basically reverse the enum
|
||||
return {
|
||||
value: name
|
||||
for name, value in cls.__dict__.items()
|
||||
if name is not None
|
||||
and not name.startswith("__")
|
||||
and not callable(name)
|
||||
and name.isupper()
|
||||
and not isinstance(value, (list, dict, tuple))
|
||||
}
|
||||
|
||||
|
||||
class SearchCategory_Photos8(SearchCategory):
|
||||
"""Search categories for Photos 8"""
|
||||
|
||||
|
||||
# Many of the category values changed in Ventura / Photos 8
|
||||
# and some new categories were added
|
||||
CITY = 5
|
||||
LOCALITY_4 = 4
|
||||
SUB_LOCALITY_5 = None
|
||||
SUB_LOCALITY_6 = 6
|
||||
LOCALITY_8 = 8
|
||||
NAMED_AREA = 7
|
||||
ALL_LOCALITY = [
|
||||
LOCALITY_4,
|
||||
SUB_LOCALITY_6,
|
||||
LOCALITY_8,
|
||||
NAMED_AREA,
|
||||
]
|
||||
HOME = 1000
|
||||
WORK = 1001
|
||||
LABEL = 1500
|
||||
MONTH = 1100
|
||||
YEAR = 1101
|
||||
@@ -253,12 +296,56 @@ class SearchCategory_Photos8(SearchCategory):
|
||||
KEYWORDS = 1200
|
||||
TITLE = 1201
|
||||
DESCRIPTION = 1202
|
||||
DETECTED_TEXT = 1203 # new in Photos 8
|
||||
DETECTED_TEXT = 1203 # new in Photos 8
|
||||
TEXT_FOUND = 1205 # new in Photos 8
|
||||
PERSON = 1300
|
||||
ACTIVITY = 1600
|
||||
VENUE = 1700
|
||||
VENUE_TYPE = 1701
|
||||
PHOTO_TYPE_VIDEO = 1901
|
||||
PHOTO_TYPE_SELFIES = 1915
|
||||
PHOTO_TYPE_LIVE = 1906
|
||||
PHOTO_TYPE_PORTRAIT = 1914
|
||||
PHOTO_TYPE_FAVORITES = 2000
|
||||
PHOTO_TYPE_PANORAMA = 1908
|
||||
PHOTO_TYPE_TIMELAPSE = 1909
|
||||
PHOTO_TYPE_SLOMO = 1905
|
||||
PHOTO_TYPE_BURSTS = 1913
|
||||
PHOTO_TYPE_SCREENSHOT = 1907
|
||||
PHOTO_TYPE_ANIMATED = 1912
|
||||
PHOTO_TYPE_RAW = 1902
|
||||
MEDIA_TYPES = [
|
||||
PHOTO_TYPE_VIDEO,
|
||||
PHOTO_TYPE_SLOMO,
|
||||
PHOTO_TYPE_LIVE,
|
||||
PHOTO_TYPE_SCREENSHOT,
|
||||
PHOTO_TYPE_PANORAMA,
|
||||
PHOTO_TYPE_TIMELAPSE,
|
||||
PHOTO_TYPE_BURSTS,
|
||||
PHOTO_TYPE_PORTRAIT,
|
||||
PHOTO_TYPE_SELFIES,
|
||||
PHOTO_TYPE_FAVORITES,
|
||||
PHOTO_TYPE_ANIMATED,
|
||||
]
|
||||
PHOTO_NAME = 2100
|
||||
CAMERA = 2300 # new in Photos 8
|
||||
CAMERA = 2300 # new in Photos 8
|
||||
SOURCE = 2200 # new in Photos 8, shows the app/software source for the photo, e.g. Messages, Safari, etc.
|
||||
|
||||
@classmethod
|
||||
def categories(cls) -> dict[int, str]:
|
||||
"""Return categories as dict of value: name"""
|
||||
# need to get the categories from the base class and update with the new values
|
||||
classdict = SearchCategory.__dict__.copy()
|
||||
classdict |= cls.__dict__.copy()
|
||||
return {
|
||||
value: name
|
||||
for name, value in classdict.items()
|
||||
if name is not None
|
||||
and not name.startswith("__")
|
||||
and not callable(name)
|
||||
and name.isupper()
|
||||
and not isinstance(value, (list, dict, tuple))
|
||||
}
|
||||
|
||||
|
||||
def search_category_factory(version: int) -> SearchCategory:
|
||||
@@ -267,7 +354,7 @@ def search_category_factory(version: int) -> SearchCategory:
|
||||
|
||||
|
||||
# Max filename length on MacOS
|
||||
MAX_FILENAME_LEN = 255
|
||||
MAX_FILENAME_LEN = 255 - len(_OSXPHOTOS_LOCK_EXTENSION)
|
||||
|
||||
# Max directory name length on MacOS
|
||||
MAX_DIRNAME_LEN = 255
|
||||
@@ -372,3 +459,7 @@ PROFILE_SORT_KEYS = [
|
||||
"time",
|
||||
"tottime",
|
||||
]
|
||||
|
||||
UUID_PATTERN = (
|
||||
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
||||
)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.54.4"
|
||||
__version__ = "0.57.3"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""cli package for osxphotos"""
|
||||
|
||||
import sys
|
||||
|
||||
from rich import print
|
||||
@@ -26,13 +27,13 @@ for func_name in args.get("--watch", []):
|
||||
wrap_function(func_name, debug_watch)
|
||||
print(f"Watching {func_name}")
|
||||
except AttributeError:
|
||||
print(f"{func_name} does not exist")
|
||||
print(f"{func_name} does not exist", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
for func_name in args.get("--breakpoint", []):
|
||||
try:
|
||||
wrap_function(func_name, debug_breakpoint)
|
||||
print(f"Breakpoint added for {func_name}")
|
||||
print(f"Breakpoint added for {func_name}", file=sys.stderr)
|
||||
except AttributeError:
|
||||
print(f"{func_name} does not exist")
|
||||
sys.exit(1)
|
||||
@@ -40,22 +41,36 @@ for func_name in args.get("--breakpoint", []):
|
||||
args = get_debug_flags(["--debug"], sys.argv)
|
||||
if args.get("--debug", False):
|
||||
set_debug(True)
|
||||
print("Debugging enabled")
|
||||
print("Debugging enabled", file=sys.stderr)
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .cli import cli_main
|
||||
from .common import get_photos_db, load_uuid_from_file
|
||||
from .cli_commands import (
|
||||
abort,
|
||||
echo,
|
||||
echo_error,
|
||||
logger,
|
||||
query_command,
|
||||
selection_command,
|
||||
verbose,
|
||||
)
|
||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION
|
||||
from .common import OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .debug_dump import debug_dump
|
||||
from .docs import docs_command
|
||||
from .dump import dump
|
||||
from .exiftool_cli import exiftool
|
||||
from .export import export
|
||||
from .exportdb import exportdb
|
||||
from .grep import grep
|
||||
from .help import help
|
||||
from .import_cli import import_cli
|
||||
from .info import info
|
||||
from .install_uninstall_run import install, run, uninstall
|
||||
from .keywords import keywords
|
||||
from .kvstore import kvstore
|
||||
from .labels import labels
|
||||
from .list import _list_libraries, list_libraries
|
||||
from .orphans import orphans
|
||||
@@ -64,40 +79,56 @@ from .photo_inspect import photo_inspect
|
||||
from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .show_command import show
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
from .uuid import uuid
|
||||
from .version import version
|
||||
|
||||
install_traceback()
|
||||
|
||||
__all__ = [
|
||||
"abort",
|
||||
"about",
|
||||
"add_locations",
|
||||
"albums",
|
||||
"cli_main",
|
||||
"debug_dump",
|
||||
"diff",
|
||||
"docs_command",
|
||||
"dump",
|
||||
"echo",
|
||||
"echo_error",
|
||||
"exiftool_cli",
|
||||
"export",
|
||||
"exportdb",
|
||||
"grep",
|
||||
"help",
|
||||
"import_cli",
|
||||
"info",
|
||||
"install",
|
||||
"keywords",
|
||||
"kvstore",
|
||||
"labels",
|
||||
"list_libraries",
|
||||
"list_libraries",
|
||||
"load_uuid_from_file",
|
||||
"logger",
|
||||
"orphans",
|
||||
"persons",
|
||||
"photo_inspect",
|
||||
"places",
|
||||
"query",
|
||||
"query_command",
|
||||
"repl",
|
||||
"run",
|
||||
"selection_command",
|
||||
"set_debug",
|
||||
"show",
|
||||
"snap",
|
||||
"tutorial",
|
||||
"uuid",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
183
osxphotos/cli/add_locations.py
Normal file
183
osxphotos/cli/add_locations.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""Add missing location data to photos in Photos.app using nearest neighbor."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
import osxphotos
|
||||
from osxphotos.queryoptions import IncompatibleQueryOptions, query_options_from_kwargs
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import QUERY_OPTIONS, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .param_types import TimeOffset
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
|
||||
def get_location(
|
||||
photos: list[osxphotos.PhotoInfo], idx: int, window: datetime.timedelta
|
||||
) -> osxphotos.PhotoInfo | None:
|
||||
"""Find nearest neighbor with location data within window of time.
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo object
|
||||
idx: index of photo in list of photos
|
||||
window: window of time to search for nearest neighbor
|
||||
|
||||
Returns:
|
||||
nearest neighbor PhotoInfo object or None if no neighbor found
|
||||
"""
|
||||
idx_back = None
|
||||
idx_forward = None
|
||||
if idx > 0:
|
||||
# search backwards in time
|
||||
for i in range(idx - 1, -1, -1):
|
||||
if (
|
||||
photos[idx].date - photos[i].date <= window
|
||||
and None not in photos[i].location
|
||||
):
|
||||
idx_back = i
|
||||
break
|
||||
|
||||
if idx < len(photos) - 1:
|
||||
# search forwards in time
|
||||
for i in range(idx + 1, len(photos)):
|
||||
if (
|
||||
photos[i].date - photos[idx].date <= window
|
||||
and None not in photos[i].location
|
||||
):
|
||||
idx_forward = i
|
||||
break
|
||||
|
||||
if idx_back is not None and idx_forward is not None:
|
||||
# found location in both directions
|
||||
# use location closest in time
|
||||
if (
|
||||
photos[idx].date - photos[idx_back].date
|
||||
< photos[idx_forward].date - photos[idx].date
|
||||
):
|
||||
return photos[idx_back]
|
||||
else:
|
||||
return photos[idx_forward]
|
||||
elif idx_back is not None:
|
||||
return photos[idx_back]
|
||||
elif idx_forward is not None:
|
||||
return photos[idx_forward]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@click.command(name="add-locations")
|
||||
@click.option(
|
||||
"--window",
|
||||
"-w",
|
||||
type=TimeOffset(),
|
||||
default="1 hr",
|
||||
help="Window of time to search for nearest neighbor; "
|
||||
"searches +/- window of time. Default is 1 hour. "
|
||||
"Format is one of 'HH:MM:SS', 'D days', 'H hours' (or hr), 'M minutes' (or min), "
|
||||
"'S seconds' (or sec), 'S' (where S is seconds).",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
help="Don't actually add location, just print what would be done. "
|
||||
"Most useful with --verbose.",
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def add_locations(
|
||||
ctx, cli_ob, window, dry_run, verbose_flag, timestamp, theme, **kwargs
|
||||
):
|
||||
"""Add missing location data to photos in Photos.app using nearest neighbor.
|
||||
|
||||
This command will search for photos that are missing location data and look
|
||||
for the nearest neighbor photo within a given window of time that contains
|
||||
location information. If a photo is found within the window of time, the
|
||||
location of the nearest neighbor will be used to update the location of the
|
||||
photo.
|
||||
|
||||
For example, if you took pictures with your iPhone and also with a camera that
|
||||
doesn't have location information, you can use this command to add location
|
||||
information to the photos taken with the camera from those taken with the
|
||||
iPhone.
|
||||
|
||||
If you have many photos with missing location information but no nearest neighbor
|
||||
within the window of time, you could add location information to some photos manually
|
||||
then run this command again to add location information to the remaining photos.
|
||||
|
||||
You can specify a subset of photos to update using the query options. For example,
|
||||
`--selected` to update only the selected photos, `--added-after 2020-01-01` to update
|
||||
only photos added after Jan 1, 2020, etc.
|
||||
|
||||
Example:
|
||||
|
||||
Add location data to all photos with missing location data within a ±2 hour window:
|
||||
|
||||
`osxphotos add-locations --window "2 hr" --verbose`
|
||||
|
||||
The add-locations command assumes that photos already have the correct date and time.
|
||||
If you have photos that are missing both location data and date/time information,
|
||||
you can use `osxphotos timewarp` to add date/time information to the photos and then
|
||||
use `osxphotos add-locations` to add location information.
|
||||
See `osxphotos help timewarp` for more information.
|
||||
"""
|
||||
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
|
||||
|
||||
verbose("Searching for photos with missing location data...")
|
||||
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions as e:
|
||||
echo_error("Incompatible query options")
|
||||
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
|
||||
ctx.exit(1)
|
||||
|
||||
photosdb = osxphotos.PhotosDB(verbose=verbose)
|
||||
photos = photosdb.query(query_options)
|
||||
|
||||
# sort photos by date
|
||||
photos = sorted(photos, key=lambda p: p.date)
|
||||
|
||||
num_photos = len(photos)
|
||||
missing_location = 0
|
||||
found_location = 0
|
||||
verbose(f"Processing {len(photos)} photos, window = ±{window}...")
|
||||
with rich_progress(console=get_verbose_console(), mock=verbose_flag) as progress:
|
||||
task = progress.add_task(
|
||||
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}, window = ±{window}",
|
||||
total=num_photos,
|
||||
)
|
||||
for idx, photo in enumerate(photos):
|
||||
if None in photo.location:
|
||||
missing_location += 1
|
||||
verbose(
|
||||
f"Processing [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
if neighbor := get_location(photos, idx, window):
|
||||
verbose(
|
||||
f"Adding location {neighbor.location} to [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
f" from [filename]{neighbor.original_filename}[/] ([uuid]{neighbor.uuid}[/])"
|
||||
)
|
||||
found_location += 1
|
||||
if not dry_run:
|
||||
photoscript.Photo(photo.uuid).location = neighbor.location
|
||||
else:
|
||||
verbose(
|
||||
f"No location found for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
progress.advance(task)
|
||||
echo(
|
||||
f"Done. Processed: [num]{num_photos}[/] photos, "
|
||||
f"missing location: [num]{missing_location}[/], "
|
||||
f"found location: [num]{found_location}[/] "
|
||||
)
|
||||
@@ -6,12 +6,12 @@ import click
|
||||
import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
@click.command()
|
||||
@DB_OPTION
|
||||
@@ -36,7 +36,8 @@ def albums(ctx, cli_obj, db, json_, photos_library):
|
||||
if photosdb.db_version > _PHOTOS_4_VERSION:
|
||||
albums["shared albums"] = photosdb.albums_shared_as_dict
|
||||
|
||||
if json_ or cli_obj.json:
|
||||
# cli_obj will be None if called from pytest
|
||||
if json_ or (cli_obj and cli_obj.json):
|
||||
click.echo(json.dumps(albums, ensure_ascii=False))
|
||||
else:
|
||||
click.echo(yaml.dump(albums, sort_keys=False, allow_unicode=True))
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""Command line interface for osxphotos """
|
||||
|
||||
import atexit
|
||||
import cProfile
|
||||
import io
|
||||
import pstats
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos._constants import PROFILE_SORT_KEYS
|
||||
from osxphotos._version import __version__
|
||||
|
||||
from .about import about
|
||||
from .add_locations import add_locations
|
||||
from .albums import albums
|
||||
from .common import DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN
|
||||
from .cli_params import DB_OPTION, DEBUG_OPTIONS, JSON_OPTION
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
from .debug_dump import debug_dump
|
||||
from .docs import docs
|
||||
from .docs import docs_command
|
||||
from .dump import dump
|
||||
from .exiftool_cli import exiftool
|
||||
from .export import export
|
||||
@@ -27,7 +35,9 @@ from .photo_inspect import photo_inspect
|
||||
from .places import places
|
||||
from .query import query
|
||||
from .repl import repl
|
||||
from .show_command import show
|
||||
from .snap_diff import diff, snap
|
||||
from .sync import sync
|
||||
from .theme import theme
|
||||
from .timewarp import timewarp
|
||||
from .tutorial import tutorial
|
||||
@@ -47,29 +57,61 @@ CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@click.group(context_settings=CTX_SETTINGS)
|
||||
@click.version_option(__version__, "--version", "-v")
|
||||
@DB_OPTION
|
||||
@JSON_OPTION
|
||||
@DEBUG_OPTIONS
|
||||
@click.option(
|
||||
"--debug",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
help="Enable debug output",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
"--profile", is_flag=True, hidden=OSXPHOTOS_HIDDEN, help="Enable profiling"
|
||||
)
|
||||
@click.option(
|
||||
"--profile-sort",
|
||||
default=None,
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
multiple=True,
|
||||
metavar="SORT_KEY",
|
||||
type=click.Choice(
|
||||
PROFILE_SORT_KEYS,
|
||||
case_sensitive=True,
|
||||
),
|
||||
help="Sort profiler output by SORT_KEY as specified at https://docs.python.org/3/library/profile.html#pstats.Stats.sort_stats. "
|
||||
f"Can be specified multiple times. Valid options are: {PROFILE_SORT_KEYS}. "
|
||||
"Default = 'cumulative'.",
|
||||
)
|
||||
@click.version_option(__version__, "--version", "-v")
|
||||
@click.pass_context
|
||||
def cli_main(ctx, db, json_, debug):
|
||||
"""osxphotos: query and export your Photos library"""
|
||||
def cli_main(ctx, db, json_, profile, profile_sort, **kwargs):
|
||||
"""osxphotos: the multi-tool for your Photos library"""
|
||||
# Note: kwargs is used to catch any debug options passed in
|
||||
# the debug options are handled in cli/__init__.py
|
||||
# before this function is called
|
||||
ctx.obj = CLI_Obj(db=db, json=json_, group=cli_main)
|
||||
if profile:
|
||||
click.echo("Profiling...")
|
||||
profile_sort = profile_sort or ["cumulative"]
|
||||
click.echo(f"Profile sort_stats order: {profile_sort}")
|
||||
pr = cProfile.Profile()
|
||||
pr.enable()
|
||||
|
||||
def at_exit():
|
||||
pr.disable()
|
||||
click.echo("Profiling completed")
|
||||
s = io.StringIO()
|
||||
pstats.Stats(pr, stream=s).strip_dirs().sort_stats(
|
||||
*profile_sort
|
||||
).print_stats()
|
||||
click.echo(s.getvalue())
|
||||
|
||||
atexit.register(at_exit)
|
||||
|
||||
|
||||
# install CLI commands
|
||||
for command in [
|
||||
about,
|
||||
add_locations,
|
||||
albums,
|
||||
debug_dump,
|
||||
diff,
|
||||
docs,
|
||||
docs_command,
|
||||
dump,
|
||||
exiftool,
|
||||
export,
|
||||
@@ -89,7 +131,9 @@ for command in [
|
||||
query,
|
||||
repl,
|
||||
run,
|
||||
show,
|
||||
snap,
|
||||
sync,
|
||||
theme,
|
||||
timewarp,
|
||||
tutorial,
|
||||
|
||||
310
osxphotos/cli/cli_commands.py
Normal file
310
osxphotos/cli/cli_commands.py
Normal file
@@ -0,0 +1,310 @@
|
||||
"""Helper functions to make writing an osxphotos CLI tool easy.
|
||||
|
||||
Includes decorator to create an osxphotos query command to be run via `osxphotos run example.py`.
|
||||
|
||||
May also be run via `python example.py` if you have pip installed osxphotos
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import typing as t # match style used in Click source code
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos.photosdb import PhotosDB
|
||||
from osxphotos.queryoptions import QueryOptions, query_options_from_kwargs
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
from .cli_params import (
|
||||
_DB_PARAMETER,
|
||||
_QUERY_PARAMETERS_DICT,
|
||||
DB_OPTION,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .click_rich_echo import set_rich_theme
|
||||
from .color_themes import get_theme
|
||||
from .verbose import verbose, verbose_print
|
||||
|
||||
logger = logging.getLogger("osxphotos")
|
||||
|
||||
# ensure echo, echo_error are configured with correct theme
|
||||
set_rich_theme(get_theme())
|
||||
|
||||
__all__ = [
|
||||
"abort",
|
||||
"echo",
|
||||
"echo_error",
|
||||
"logger",
|
||||
"query_command",
|
||||
"selection_command",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
|
||||
def abort(message: str, exit_code: int = 1):
|
||||
"""Abort with error message and exit code"""
|
||||
echo_error(f"[error]{message}[/]")
|
||||
sys.exit(exit_code)
|
||||
|
||||
|
||||
def config_verbose_callback(ctx: click.Context, param: click.Parameter, value: t.Any):
|
||||
"""Callback for --verbose option"""
|
||||
# calling verbose_print() will set the verbose level for the verbose() function
|
||||
theme = ctx.params.get("theme")
|
||||
timestamp = ctx.params.get("timestamp")
|
||||
verbose_print(verbose=value, timestamp=timestamp, theme=theme)
|
||||
return value
|
||||
|
||||
|
||||
def get_photos_for_query(ctx: click.Context):
|
||||
"""Return list of PhotoInfo objects for the photos matching the query options in ctx.params"""
|
||||
options = query_options_from_kwargs(**ctx.params)
|
||||
db = ctx.params.get("db")
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
return photosdb.query(options=options)
|
||||
|
||||
|
||||
def get_selected_photos(ctx: click.Context):
|
||||
"""Return list of PhotoInfo objects for the photos currently selected in Photos.app"""
|
||||
photosdb = PhotosDB(verbose=verbose)
|
||||
return photosdb.query(options=QueryOptions(selected=True))
|
||||
|
||||
|
||||
class QueryCommand(click.Command):
|
||||
"""
|
||||
Click command to create an osxphotos query command.
|
||||
|
||||
This class is used by the query_command decorator to create a click command
|
||||
that runs an osxphotos query. It will automatically add the query options as
|
||||
well as the --verbose, --timestamp, --theme, and --db options.
|
||||
"""
|
||||
|
||||
standalone_mode = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: t.Optional[str],
|
||||
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
params: t.Optional[t.List[click.Parameter]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
epilog: t.Optional[str] = None,
|
||||
short_help: t.Optional[str] = None,
|
||||
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||
add_help_option: bool = True,
|
||||
no_args_is_help: bool = False,
|
||||
hidden: bool = False,
|
||||
deprecated: bool = False,
|
||||
) -> None:
|
||||
self.params = params or []
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
callback=config_verbose_callback,
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--timestamp"],
|
||||
is_flag=True,
|
||||
help="Add time stamp to verbose output",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(
|
||||
["dark", "light", "mono", "plain"], case_sensitive=False
|
||||
),
|
||||
help="Specify the color theme to use for output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
)
|
||||
self.params.append(_DB_PARAMETER)
|
||||
self.params.extend(_QUERY_PARAMETERS_DICT.values())
|
||||
|
||||
super().__init__(
|
||||
name,
|
||||
context_settings,
|
||||
callback,
|
||||
self.params,
|
||||
help,
|
||||
epilog,
|
||||
short_help,
|
||||
options_metavar,
|
||||
add_help_option,
|
||||
no_args_is_help,
|
||||
hidden,
|
||||
deprecated,
|
||||
)
|
||||
|
||||
def make_context(
|
||||
self,
|
||||
info_name: t.Optional[str],
|
||||
args: t.List[str],
|
||||
parent: t.Optional[click.Context] = None,
|
||||
**extra: t.Any,
|
||||
) -> click.Context:
|
||||
ctx = super().make_context(info_name, args, parent, **extra)
|
||||
ctx.obj = self
|
||||
photos = get_photos_for_query(ctx)
|
||||
ctx.params["photos"] = photos
|
||||
|
||||
# remove params handled by this class
|
||||
ctx.params.pop("verbose_flag")
|
||||
ctx.params.pop("timestamp")
|
||||
ctx.params.pop("theme")
|
||||
return ctx
|
||||
|
||||
|
||||
class SelectionCommand(click.Command):
|
||||
"""
|
||||
Click command to create an osxphotos selection command that runs on selected photos.
|
||||
|
||||
This class is used by the query_command decorator to create a click command
|
||||
that runs on currently selected photos.
|
||||
|
||||
The --verbose, --timestamp, --theme, and --db options will also be added to the command.
|
||||
"""
|
||||
|
||||
standalone_mode = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: t.Optional[str],
|
||||
context_settings: t.Optional[t.Dict[str, t.Any]] = None,
|
||||
callback: t.Optional[t.Callable[..., t.Any]] = None,
|
||||
params: t.Optional[t.List[click.Parameter]] = None,
|
||||
help: t.Optional[str] = None,
|
||||
epilog: t.Optional[str] = None,
|
||||
short_help: t.Optional[str] = None,
|
||||
options_metavar: t.Optional[str] = "[OPTIONS]",
|
||||
add_help_option: bool = True,
|
||||
no_args_is_help: bool = False,
|
||||
hidden: bool = False,
|
||||
deprecated: bool = False,
|
||||
) -> None:
|
||||
self.params = params or []
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
callback=config_verbose_callback,
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--timestamp"],
|
||||
is_flag=True,
|
||||
help="Add time stamp to verbose output",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(
|
||||
["dark", "light", "mono", "plain"], case_sensitive=False
|
||||
),
|
||||
help="Specify the color theme to use for output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
)
|
||||
self.params.append(
|
||||
click.Option(
|
||||
param_decls=["--library", "--db"],
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify Photos database path. "
|
||||
"Path to Photos library/database can be specified using either --db "
|
||||
"or directly as PHOTOS_LIBRARY positional argument. "
|
||||
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
|
||||
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
)
|
||||
super().__init__(
|
||||
name,
|
||||
context_settings,
|
||||
callback,
|
||||
self.params,
|
||||
help,
|
||||
epilog,
|
||||
short_help,
|
||||
options_metavar,
|
||||
add_help_option,
|
||||
no_args_is_help,
|
||||
hidden,
|
||||
deprecated,
|
||||
)
|
||||
|
||||
def make_context(
|
||||
self,
|
||||
info_name: t.Optional[str],
|
||||
args: t.List[str],
|
||||
parent: t.Optional[click.Context] = None,
|
||||
**extra: t.Any,
|
||||
) -> click.Context:
|
||||
ctx = super().make_context(info_name, args, parent, **extra)
|
||||
ctx.obj = self
|
||||
photos = get_selected_photos(ctx)
|
||||
ctx.params["photos"] = photos
|
||||
|
||||
# remove params handled by this class
|
||||
ctx.params.pop("verbose_flag")
|
||||
ctx.params.pop("timestamp")
|
||||
ctx.params.pop("theme")
|
||||
return ctx
|
||||
|
||||
|
||||
def query_command(name=None, cls=QueryCommand, **attrs):
|
||||
"""Decorator to create an osxphotos command to be run via `osxphotos run example.py`
|
||||
|
||||
The command will be passed a list of PhotoInfo objects for all photos in Photos
|
||||
matching the query options or all photos if no query options are specified.
|
||||
|
||||
The standard osxphotos query options will be added to the command.
|
||||
|
||||
The CLI will also be passed the following options:
|
||||
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
"""
|
||||
if callable(name) and cls:
|
||||
return click.command(cls=cls, **attrs)(name)
|
||||
|
||||
return click.command(name, cls=cls, **attrs)
|
||||
|
||||
|
||||
def selection_command(name=None, cls=SelectionCommand, **attrs):
|
||||
"""Decorator to create an osxphotos command to be run via `osxphotos run example.py`
|
||||
|
||||
The command will be passed a list of PhotoInfo objects for all photos selected in Photos.
|
||||
The CLI will also be passed the following options:
|
||||
|
||||
--verbose
|
||||
--timestamp
|
||||
--theme
|
||||
--db
|
||||
"""
|
||||
if callable(name) and cls:
|
||||
return click.command(cls=cls, **attrs)(name)
|
||||
|
||||
return click.command(name, cls=cls, **attrs)
|
||||
678
osxphotos/cli/cli_params.py
Normal file
678
osxphotos/cli/cli_params.py
Normal file
@@ -0,0 +1,678 @@
|
||||
"""Common options & parameters for osxphotos CLI commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import Any, Callable
|
||||
|
||||
import click
|
||||
|
||||
from .common import OSXPHOTOS_HIDDEN
|
||||
from .param_types import *
|
||||
|
||||
__all__ = [
|
||||
"DB_ARGUMENT",
|
||||
"DB_OPTION",
|
||||
"DEBUG_OPTIONS",
|
||||
"DELETED_OPTIONS",
|
||||
"FIELD_OPTION",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"VERBOSE_OPTION",
|
||||
"TIMESTAMP_OPTION",
|
||||
]
|
||||
|
||||
|
||||
def _param_memo(f: Callable[..., Any], param: click.Parameter) -> None:
|
||||
"""Add param to the list of params for a click.Command
|
||||
This is directly from the click source code and
|
||||
the implementation is thus tightly coupled to click internals
|
||||
"""
|
||||
if isinstance(f, click.Command):
|
||||
f.params.append(param)
|
||||
else:
|
||||
if not hasattr(f, "__click_params__"):
|
||||
f.__click_params__ = [] # type: ignore
|
||||
|
||||
f.__click_params__.append(param) # type: ignore
|
||||
|
||||
|
||||
def make_click_option_decorator(*params: click.Parameter) -> Callable[..., Any]:
|
||||
"""Make a decorator for a click option from one or more click Parameter objects"""
|
||||
|
||||
def decorator(wrapped=None) -> Callable[..., Any]:
|
||||
"""Function decorator to add option to a click command.
|
||||
|
||||
Args:
|
||||
wrapped: function to decorate (this is normally passed automatically
|
||||
"""
|
||||
|
||||
if wrapped is None:
|
||||
return decorator
|
||||
|
||||
def _add_options(wrapped):
|
||||
"""Add query options to wrapped function"""
|
||||
for param in params:
|
||||
_param_memo(wrapped, param)
|
||||
return wrapped
|
||||
|
||||
return _add_options(wrapped)
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
VERSION_CHECK_OPTION = click.option("--no-version-check", required=False, is_flag=True)
|
||||
|
||||
_DB_PARAMETER = click.Option(
|
||||
["--library", "--db", "db"],
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify path to Photos library. "
|
||||
"If not provided, will attempt to find the library to use in the following order: "
|
||||
"1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
|
||||
DB_OPTION = make_click_option_decorator(_DB_PARAMETER)
|
||||
|
||||
DB_ARGUMENT = click.argument(
|
||||
"photos_library",
|
||||
nargs=-1,
|
||||
type=DeprecatedPath(
|
||||
exists=True,
|
||||
deprecation_warning="The PHOTOS_LIBRARY argument is deprecated and "
|
||||
"will be removed in a future version of osxphotos. "
|
||||
"Use --library instead to specify the Photos Library path.",
|
||||
),
|
||||
)
|
||||
|
||||
_JSON_PARAMETER = click.Option(
|
||||
["--json", "json_"],
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format.",
|
||||
)
|
||||
|
||||
JSON_OPTION = make_click_option_decorator(_JSON_PARAMETER)
|
||||
|
||||
_FIELD_PARAMETER = click.Option(
|
||||
["--field", "-f"],
|
||||
metavar="FIELD TEMPLATE",
|
||||
multiple=True,
|
||||
nargs=2,
|
||||
help="Output only specified custom fields. "
|
||||
"FIELD is the name of the field and TEMPLATE is the template to use as the field value. "
|
||||
"May be repeated to output multiple fields. "
|
||||
"For example, to output photo uuid, name, and title: "
|
||||
'`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.',
|
||||
)
|
||||
|
||||
FIELD_OPTION = make_click_option_decorator(_FIELD_PARAMETER)
|
||||
|
||||
_DELETED_PARAMETERS = [
|
||||
click.Option(
|
||||
["--deleted"],
|
||||
is_flag=True,
|
||||
help="Include photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
click.Option(
|
||||
["--deleted-only"],
|
||||
is_flag=True,
|
||||
help="Include only photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
]
|
||||
|
||||
DELETED_OPTIONS = make_click_option_decorator(*_DELETED_PARAMETERS)
|
||||
|
||||
# The following are used by the query command and by
|
||||
# QUERY_OPTIONS to add the query options to other commands
|
||||
# To add new query options, add them to _QUERY_OPTIONS as
|
||||
# a click.Option, add them to osxphotos/photosdb/photosdb.py::PhotosDB.query(),
|
||||
# and to osxphotos/query_options.py::QueryOptions
|
||||
_QUERY_PARAMETERS_DICT = {
|
||||
"--keyword": click.Option(
|
||||
["--keyword"],
|
||||
metavar="KEYWORD",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with keyword KEYWORD. "
|
||||
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
|
||||
),
|
||||
"--no-keyword": click.Option(
|
||||
["--no-keyword"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no keyword.",
|
||||
),
|
||||
"--person": click.Option(
|
||||
["--person"],
|
||||
metavar="PERSON",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with person PERSON. "
|
||||
'If more than one person, treated as "OR", e.g. find photos matching any person',
|
||||
),
|
||||
"--album": click.Option(
|
||||
["--album"],
|
||||
metavar="ALBUM",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in album ALBUM. "
|
||||
'If more than one album, treated as "OR", e.g. find photos matching any album',
|
||||
),
|
||||
"--folder": click.Option(
|
||||
["--folder"],
|
||||
metavar="FOLDER",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="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": click.Option(
|
||||
["--name"],
|
||||
metavar="FILENAME",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="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": click.Option(
|
||||
["--uuid"],
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
"--uuid-from-file": click.Option(
|
||||
["--uuid-from-file"],
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos with UUID(s) loaded from FILE. "
|
||||
"Format is a single UUID per line. Lines preceded with # are ignored. "
|
||||
"If FILE is '-', read UUIDs from stdin.",
|
||||
type=PathOrStdin(exists=True),
|
||||
),
|
||||
"--title": click.Option(
|
||||
["--title"],
|
||||
metavar="TITLE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for TITLE in title of photo.",
|
||||
),
|
||||
"--no-title": click.Option(
|
||||
["--no-title"], is_flag=True, help="Search for photos with no title."
|
||||
),
|
||||
"--description": click.Option(
|
||||
["--description"],
|
||||
metavar="DESC",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for DESC in description of photo.",
|
||||
),
|
||||
"--no-description": click.Option(
|
||||
["--no-description"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no description.",
|
||||
),
|
||||
"--place": click.Option(
|
||||
["--place"],
|
||||
metavar="PLACE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for PLACE in photo's reverse geolocation info",
|
||||
),
|
||||
"--no-place": click.Option(
|
||||
["--no-place"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
||||
),
|
||||
"--location": click.Option(
|
||||
["--location"],
|
||||
is_flag=True,
|
||||
help="Search for photos with associated location info (e.g. GPS coordinates)",
|
||||
),
|
||||
"--no-location": click.Option(
|
||||
["--no-location"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
|
||||
),
|
||||
"--label": click.Option(
|
||||
["--label"],
|
||||
metavar="LABEL",
|
||||
multiple=True,
|
||||
help="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": click.Option(
|
||||
["--uti"],
|
||||
metavar="UTI",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos whose uniform type identifier (UTI) matches UTI",
|
||||
),
|
||||
"--ignore_case": click.Option(
|
||||
["-i", "--ignore-case"],
|
||||
is_flag=True,
|
||||
help="Case insensitive search for title, description, place, keyword, person, or album.",
|
||||
),
|
||||
"--edited": click.Option(
|
||||
["--edited"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have been edited.",
|
||||
),
|
||||
"--not-edited": click.Option(
|
||||
["--not-edited"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have not been edited.",
|
||||
),
|
||||
"--external-edit": click.Option(
|
||||
["--external-edit"],
|
||||
is_flag=True,
|
||||
help="Search for photos edited in external editor.",
|
||||
),
|
||||
"--favorite": click.Option(
|
||||
["--favorite"], is_flag=True, help="Search for photos marked favorite."
|
||||
),
|
||||
"--not-favorite": click.Option(
|
||||
["--not-favorite"],
|
||||
is_flag=True,
|
||||
help="Search for photos not marked favorite.",
|
||||
),
|
||||
"--hidden": click.Option(
|
||||
["--hidden"], is_flag=True, help="Search for photos marked hidden."
|
||||
),
|
||||
"--not-hidden": click.Option(
|
||||
["--not-hidden"],
|
||||
is_flag=True,
|
||||
help="Search for photos not marked hidden.",
|
||||
),
|
||||
"--shared": click.Option(
|
||||
["--shared"],
|
||||
is_flag=True,
|
||||
help="Search for photos in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
"--not-shared": click.Option(
|
||||
["--not-shared"],
|
||||
is_flag=True,
|
||||
help="Search for photos not in shared iCloud album (Photos 5+ only).",
|
||||
),
|
||||
"--burst": click.Option(
|
||||
["--burst"],
|
||||
is_flag=True,
|
||||
help="Search for photos that were taken in a burst.",
|
||||
),
|
||||
"--not-burst": click.Option(
|
||||
["--not-burst"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of a burst.",
|
||||
),
|
||||
"--live": click.Option(
|
||||
["--live"], is_flag=True, help="Search for Apple live photos"
|
||||
),
|
||||
"--not-live": click.Option(
|
||||
["--not-live"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple live photos.",
|
||||
),
|
||||
"--portrait": click.Option(
|
||||
["--portrait"],
|
||||
is_flag=True,
|
||||
help="Search for Apple portrait mode photos.",
|
||||
),
|
||||
"--not-portrait": click.Option(
|
||||
["--not-portrait"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple portrait mode photos.",
|
||||
),
|
||||
"--screenshot": click.Option(
|
||||
["--screenshot"], is_flag=True, help="Search for screenshot photos."
|
||||
),
|
||||
"--not-screenshot": click.Option(
|
||||
["--not-screenshot"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not screenshot photos.",
|
||||
),
|
||||
"--slow-mo": click.Option(
|
||||
["--slow-mo"], is_flag=True, help="Search for slow motion videos."
|
||||
),
|
||||
"--not-slow-mo": click.Option(
|
||||
["--not-slow-mo"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not slow motion videos.",
|
||||
),
|
||||
"--time-lapse": click.Option(
|
||||
["--time-lapse"], is_flag=True, help="Search for time lapse videos."
|
||||
),
|
||||
"--not-time-lapse": click.Option(
|
||||
["--not-time-lapse"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not time lapse videos.",
|
||||
),
|
||||
"--hdr": click.Option(
|
||||
["--hdr"],
|
||||
is_flag=True,
|
||||
help="Search for high dynamic range (HDR) photos.",
|
||||
),
|
||||
"--not-hdr": click.Option(
|
||||
["--not-hdr"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not HDR photos.",
|
||||
),
|
||||
"--selfie": click.Option(
|
||||
["--selfie"],
|
||||
is_flag=True,
|
||||
help="Search for selfies (photos taken with front-facing cameras).",
|
||||
),
|
||||
"--not-selfie": click.Option(
|
||||
["--not-selfie"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not selfies.",
|
||||
),
|
||||
"--panorama": click.Option(
|
||||
["--panorama"], is_flag=True, help="Search for panorama photos."
|
||||
),
|
||||
"--not-panorama": click.Option(
|
||||
["--not-panorama"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not panoramas.",
|
||||
),
|
||||
"--has-raw": click.Option(
|
||||
["--has-raw"],
|
||||
is_flag=True,
|
||||
help="Search for photos with both a jpeg and raw version",
|
||||
),
|
||||
"--only-movies": click.Option(
|
||||
["--only-movies"],
|
||||
is_flag=True,
|
||||
help="Search only for movies (default searches both images and movies).",
|
||||
),
|
||||
"--only-photos": click.Option(
|
||||
["--only-photos"],
|
||||
is_flag=True,
|
||||
help="Search only for photos/images (default searches both images and movies).",
|
||||
),
|
||||
"--from-date": click.Option(
|
||||
["--from-date"],
|
||||
help="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).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--to-date": click.Option(
|
||||
["--to-date"],
|
||||
help="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).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--from-time": click.Option(
|
||||
["--from-time"],
|
||||
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
"--to-time": click.Option(
|
||||
["--to-time"],
|
||||
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
"--year": click.Option(
|
||||
["--year"],
|
||||
metavar="YEAR",
|
||||
help="Search for items from a specific year, e.g. --year 2022 to find all photos from the year 2022. "
|
||||
"May be repeated to search multiple years.",
|
||||
multiple=True,
|
||||
type=int,
|
||||
),
|
||||
"--added-before": click.Option(
|
||||
["--added-before"],
|
||||
metavar="DATE",
|
||||
help="Search for items added to the library before a specific date/time, "
|
||||
"e.g. --added-before e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--added-after": click.Option(
|
||||
["--added-after"],
|
||||
metavar="DATE",
|
||||
help="Search for items added to the libray after a specific date/time, "
|
||||
"e.g. --added-after e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
"--added-in-last": click.Option(
|
||||
["--added-in-last"],
|
||||
metavar="TIME_DELTA",
|
||||
help="Search for items added to the library in the last TIME_DELTA, "
|
||||
"where TIME_DELTA is a string like "
|
||||
"'12 hrs', '1 day', '1d', '1 week', '2weeks', '1 month', '1 year'. "
|
||||
"for example, `--added-in-last 7d` and `--added-in-last '1 week'` are equivalent. "
|
||||
"months are assumed to be 30 days and years are assumed to be 365 days. "
|
||||
"Common English abbreviations are accepted, e.g. d, day, days or m, min, minutes.",
|
||||
type=TimeOffset(),
|
||||
),
|
||||
"--has-comment": click.Option(
|
||||
["--has-comment"],
|
||||
is_flag=True,
|
||||
help="Search for photos that have comments.",
|
||||
),
|
||||
"--no-comment": click.Option(
|
||||
["--no-comment"],
|
||||
is_flag=True,
|
||||
help="Search for photos with no comments.",
|
||||
),
|
||||
"--has-likes": click.Option(
|
||||
["--has-likes"], is_flag=True, help="Search for photos that have likes."
|
||||
),
|
||||
"--no-likes": click.Option(
|
||||
["--no-likes"], is_flag=True, help="Search for photos with no likes."
|
||||
),
|
||||
"--is-reference": click.Option(
|
||||
["--is-reference"],
|
||||
is_flag=True,
|
||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||
),
|
||||
"--not-reference": click.Option(
|
||||
["--not-reference"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not references, that is, they were copied into the Photos library "
|
||||
"and are managed by Photos.",
|
||||
),
|
||||
"--in-album": click.Option(
|
||||
["--in-album"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are in one or more albums.",
|
||||
),
|
||||
"--not-in-album": click.Option(
|
||||
["--not-in-album"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in any albums.",
|
||||
),
|
||||
"--duplicate": click.Option(
|
||||
["--duplicate"],
|
||||
is_flag=True,
|
||||
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
|
||||
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
|
||||
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
|
||||
"times or duplicated within Photos.",
|
||||
),
|
||||
"--min-size": click.Option(
|
||||
["--min-size"],
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size >= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
"--max-size": click.Option(
|
||||
["--max-size"],
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size <= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
"--missing": click.Option(
|
||||
["--missing"], is_flag=True, help="Search for photos missing from disk."
|
||||
),
|
||||
"--not-missing": click.Option(
|
||||
["--not-missing"],
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
),
|
||||
"--cloudasset": click.Option(
|
||||
["--cloudasset"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
),
|
||||
"--not-cloudasset": click.Option(
|
||||
["--not-cloudasset"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
),
|
||||
"--incloud": click.Option(
|
||||
["--incloud"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
),
|
||||
"--not-incloud": click.Option(
|
||||
["--not-incloud"],
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
),
|
||||
"--regex": click.Option(
|
||||
["--regex"],
|
||||
metavar="REGEX TEMPLATE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="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.",
|
||||
),
|
||||
"--selected": click.Option(
|
||||
["--selected"],
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
"--exif": click.Option(
|
||||
["--exif"],
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
"--query-eval": click.Option(
|
||||
["--query-eval"],
|
||||
metavar="CRITERIA",
|
||||
multiple=True,
|
||||
help="Evaluate CRITERIA to filter photos. "
|
||||
"CRITERIA will be evaluated in context of the following python list comprehension: "
|
||||
"`photos = [photo for photo in photos if CRITERIA]` "
|
||||
"where photo represents a PhotoInfo object. "
|
||||
"For example: `--query-eval photo.favorite` returns all photos that have been "
|
||||
"favorited and is equivalent to --favorite. "
|
||||
"You may specify more than one CRITERIA by using --query-eval multiple times. "
|
||||
"CRITERIA must be a valid python expression. "
|
||||
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
),
|
||||
"--query-function": click.Option(
|
||||
["--query-function"],
|
||||
metavar="filename.py::function",
|
||||
multiple=True,
|
||||
type=FunctionCall(),
|
||||
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
|
||||
+ "file you've created and function is the name of the function in the python file you want to call. "
|
||||
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
|
||||
+ "You may use more than one function by repeating the --query-function option with a different value. "
|
||||
+ "Your query function will be called after all other query options have been evaluated. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def QUERY_OPTIONS(
|
||||
wrapped=None, *, exclude: list[str] | None = None
|
||||
) -> Callable[..., Any]:
|
||||
"""Function decorator to add query options to a click command.
|
||||
|
||||
Args:
|
||||
wrapped: function to decorate (this is normally passed automatically
|
||||
exclude: list of query options to exclude from the command, for example `exclude=["--shared"]
|
||||
"""
|
||||
if wrapped is None:
|
||||
return functools.partial(QUERY_OPTIONS, exclude=exclude)
|
||||
|
||||
exclude = exclude or []
|
||||
|
||||
def _add_options(wrapped):
|
||||
"""Add query options to wrapped function"""
|
||||
for option in reversed(_QUERY_PARAMETERS_DICT.keys()):
|
||||
if option in exclude:
|
||||
continue
|
||||
click_opt = _QUERY_PARAMETERS_DICT[option]
|
||||
_param_memo(wrapped, click_opt)
|
||||
return wrapped
|
||||
|
||||
return _add_options(wrapped)
|
||||
|
||||
|
||||
_DEBUG_PARAMETERS = [
|
||||
click.Option(
|
||||
["--debug"],
|
||||
is_flag=True,
|
||||
help="Enable debug output.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
click.Option(
|
||||
["--watch"],
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Watch function or method calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to watch all calls to FileUtil.copy() which is in "
|
||||
"osxphotos.fileutil, use: "
|
||||
"'--watch osxphotos.fileutil::FileUtil.copy'. More than one --watch option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
click.Option(
|
||||
["--breakpoint"],
|
||||
metavar="MODULE::NAME",
|
||||
multiple=True,
|
||||
help="Add breakpoint to function calls. The function to watch must be in the form "
|
||||
"MODULE::NAME where MODULE is the module path and NAME is the function or method name "
|
||||
"contained in the module. For example, to set a breakpoint for calls to "
|
||||
"FileUtil.copy() which is in osxphotos.fileutil, use: "
|
||||
"'--breakpoint osxphotos.fileutil::FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
]
|
||||
DEBUG_OPTIONS = make_click_option_decorator(*_DEBUG_PARAMETERS)
|
||||
|
||||
_THEME_PARAMETER = click.Option(
|
||||
["--theme"],
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
THEME_OPTION = make_click_option_decorator(_THEME_PARAMETER)
|
||||
|
||||
_VERBOSE_PARAMETER = click.Option(
|
||||
["--verbose", "-V", "verbose_flag"],
|
||||
count=True,
|
||||
help="Print verbose output; may be specified multiple times for more verbose output.",
|
||||
)
|
||||
VERBOSE_OPTION = make_click_option_decorator(_VERBOSE_PARAMETER)
|
||||
|
||||
_TIMESTAMP_PARAMETER = click.Option(
|
||||
["--timestamp"], is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
TIMESTAMP_OPTION = make_click_option_decorator(_TIMESTAMP_PARAMETER)
|
||||
@@ -119,7 +119,7 @@ def rich_echo(
|
||||
# if not outputting to terminal, use a huge width to avoid wrapping
|
||||
# otherwise tests fail
|
||||
width = 10_000
|
||||
console = get_rich_console() or Console(theme=theme, width=width)
|
||||
console = get_rich_console() or Console(theme=theme or get_rich_theme(), width=width)
|
||||
if markdown:
|
||||
message = Markdown(message)
|
||||
# Markdown always adds a new line so disable unless explicitly specified
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Globals and constants use by the CLI commands"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
@@ -14,8 +15,6 @@ from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.utils import get_latest_version
|
||||
|
||||
from .param_types import *
|
||||
|
||||
# used to show/hide hidden commands
|
||||
OSXPHOTOS_HIDDEN = not bool(os.getenv("OSXPHOTOS_SHOW_HIDDEN", default=False))
|
||||
|
||||
@@ -31,16 +30,7 @@ CLI_COLOR_WARNING = "yellow"
|
||||
__all__ = [
|
||||
"CLI_COLOR_ERROR",
|
||||
"CLI_COLOR_WARNING",
|
||||
"DB_ARGUMENT",
|
||||
"DB_OPTION",
|
||||
"DEBUG_OPTIONS",
|
||||
"DELETED_OPTIONS",
|
||||
"FIELD_OPTION",
|
||||
"JSON_OPTION",
|
||||
"QUERY_OPTIONS",
|
||||
"THEME_OPTION",
|
||||
"get_photos_db",
|
||||
"load_uuid_from_file",
|
||||
"noop",
|
||||
"time_stamp",
|
||||
]
|
||||
@@ -89,518 +79,6 @@ def get_photos_db(*db_options):
|
||||
return None
|
||||
|
||||
|
||||
VERSION_CHECK_OPTION = click.option("--no-version-check", required=False, is_flag=True)
|
||||
|
||||
DB_OPTION = click.option(
|
||||
"--db",
|
||||
required=False,
|
||||
metavar="PHOTOS_LIBRARY_PATH",
|
||||
default=None,
|
||||
help=(
|
||||
"Specify Photos database path. "
|
||||
"Path to Photos library/database can be specified using either --db "
|
||||
"or directly as PHOTOS_LIBRARY positional argument. "
|
||||
"If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library "
|
||||
"to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary"
|
||||
),
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
|
||||
DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
|
||||
|
||||
JSON_OPTION = click.option(
|
||||
"--json",
|
||||
"json_",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format.",
|
||||
)
|
||||
|
||||
FIELD_OPTION = click.option(
|
||||
"--field",
|
||||
"-f",
|
||||
metavar="FIELD TEMPLATE",
|
||||
multiple=True,
|
||||
nargs=2,
|
||||
help="Output only specified custom fields. "
|
||||
"FIELD is the name of the field and TEMPLATE is the template to use as the field value. "
|
||||
"May be repeated to output multiple fields. "
|
||||
"For example, to output photo uuid, name, and title: "
|
||||
'`--field uuid "{uuid}" --field name "{original_name}" --field title "{title}"`.',
|
||||
)
|
||||
|
||||
|
||||
def DELETED_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--deleted",
|
||||
is_flag=True,
|
||||
help="Include photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
o(
|
||||
"--deleted-only",
|
||||
is_flag=True,
|
||||
help="Include only photos from the 'Recently Deleted' folder.",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
def QUERY_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--keyword",
|
||||
metavar="KEYWORD",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with keyword KEYWORD. "
|
||||
'If more than one keyword, treated as "OR", e.g. find photos matching any keyword',
|
||||
),
|
||||
o(
|
||||
"--no-keyword",
|
||||
is_flag=True,
|
||||
help="Search for photos with no keyword.",
|
||||
),
|
||||
o(
|
||||
"--person",
|
||||
metavar="PERSON",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with person PERSON. "
|
||||
'If more than one person, treated as "OR", e.g. find photos matching any person',
|
||||
),
|
||||
o(
|
||||
"--album",
|
||||
metavar="ALBUM",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos in album ALBUM. "
|
||||
'If more than one album, treated as "OR", e.g. find photos matching any album',
|
||||
),
|
||||
o(
|
||||
"--folder",
|
||||
metavar="FOLDER",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="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)",
|
||||
),
|
||||
o(
|
||||
"--name",
|
||||
metavar="FILENAME",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="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. ",
|
||||
),
|
||||
o(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for photos with UUID(s). "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
),
|
||||
o(
|
||||
"--uuid-from-file",
|
||||
metavar="FILE",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos with UUID(s) loaded from FILE. "
|
||||
"Format is a single UUID per line. Lines preceded with # are ignored.",
|
||||
type=click.Path(exists=True),
|
||||
),
|
||||
o(
|
||||
"--title",
|
||||
metavar="TITLE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for TITLE in title of photo.",
|
||||
),
|
||||
o("--no-title", is_flag=True, help="Search for photos with no title."),
|
||||
o(
|
||||
"--description",
|
||||
metavar="DESC",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for DESC in description of photo.",
|
||||
),
|
||||
o(
|
||||
"--no-description",
|
||||
is_flag=True,
|
||||
help="Search for photos with no description.",
|
||||
),
|
||||
o(
|
||||
"--place",
|
||||
metavar="PLACE",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="Search for PLACE in photo's reverse geolocation info",
|
||||
),
|
||||
o(
|
||||
"--no-place",
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated place name info (no reverse geolocation info)",
|
||||
),
|
||||
o(
|
||||
"--location",
|
||||
is_flag=True,
|
||||
help="Search for photos with associated location info (e.g. GPS coordinates)",
|
||||
),
|
||||
o(
|
||||
"--no-location",
|
||||
is_flag=True,
|
||||
help="Search for photos with no associated location info (e.g. no GPS coordinates)",
|
||||
),
|
||||
o(
|
||||
"--label",
|
||||
metavar="LABEL",
|
||||
multiple=True,
|
||||
help="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',
|
||||
),
|
||||
o(
|
||||
"--uti",
|
||||
metavar="UTI",
|
||||
default=None,
|
||||
multiple=False,
|
||||
help="Search for photos whose uniform type identifier (UTI) matches UTI",
|
||||
),
|
||||
o(
|
||||
"-i",
|
||||
"--ignore-case",
|
||||
is_flag=True,
|
||||
help="Case insensitive search for title, description, place, keyword, person, or album.",
|
||||
),
|
||||
o("--edited", is_flag=True, help="Search for photos that have been edited."),
|
||||
o(
|
||||
"--external-edit",
|
||||
is_flag=True,
|
||||
help="Search for photos edited in external editor.",
|
||||
),
|
||||
o("--favorite", is_flag=True, help="Search for photos marked favorite."),
|
||||
o(
|
||||
"--not-favorite",
|
||||
is_flag=True,
|
||||
help="Search for photos not marked favorite.",
|
||||
),
|
||||
o("--hidden", is_flag=True, help="Search for photos marked hidden."),
|
||||
o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."),
|
||||
o(
|
||||
"--shared",
|
||||
is_flag=True,
|
||||
help="Search for photos in shared iCloud album (Photos 5 only).",
|
||||
),
|
||||
o(
|
||||
"--not-shared",
|
||||
is_flag=True,
|
||||
help="Search for photos not in shared iCloud album (Photos 5 only).",
|
||||
),
|
||||
o(
|
||||
"--burst",
|
||||
is_flag=True,
|
||||
help="Search for photos that were taken in a burst.",
|
||||
),
|
||||
o(
|
||||
"--not-burst",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of a burst.",
|
||||
),
|
||||
o("--live", is_flag=True, help="Search for Apple live photos"),
|
||||
o(
|
||||
"--not-live",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple live photos.",
|
||||
),
|
||||
o("--portrait", is_flag=True, help="Search for Apple portrait mode photos."),
|
||||
o(
|
||||
"--not-portrait",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not Apple portrait mode photos.",
|
||||
),
|
||||
o("--screenshot", is_flag=True, help="Search for screenshot photos."),
|
||||
o(
|
||||
"--not-screenshot",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not screenshot photos.",
|
||||
),
|
||||
o("--slow-mo", is_flag=True, help="Search for slow motion videos."),
|
||||
o(
|
||||
"--not-slow-mo",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not slow motion videos.",
|
||||
),
|
||||
o("--time-lapse", is_flag=True, help="Search for time lapse videos."),
|
||||
o(
|
||||
"--not-time-lapse",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not time lapse videos.",
|
||||
),
|
||||
o("--hdr", is_flag=True, help="Search for high dynamic range (HDR) photos."),
|
||||
o("--not-hdr", is_flag=True, help="Search for photos that are not HDR photos."),
|
||||
o(
|
||||
"--selfie",
|
||||
is_flag=True,
|
||||
help="Search for selfies (photos taken with front-facing cameras).",
|
||||
),
|
||||
o("--not-selfie", is_flag=True, help="Search for photos that are not selfies."),
|
||||
o("--panorama", is_flag=True, help="Search for panorama photos."),
|
||||
o(
|
||||
"--not-panorama",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not panoramas.",
|
||||
),
|
||||
o(
|
||||
"--has-raw",
|
||||
is_flag=True,
|
||||
help="Search for photos with both a jpeg and raw version",
|
||||
),
|
||||
o(
|
||||
"--only-movies",
|
||||
is_flag=True,
|
||||
help="Search only for movies (default searches both images and movies).",
|
||||
),
|
||||
o(
|
||||
"--only-photos",
|
||||
is_flag=True,
|
||||
help="Search only for photos/images (default searches both images and movies).",
|
||||
),
|
||||
o(
|
||||
"--from-date",
|
||||
help="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).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--to-date",
|
||||
help="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).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--from-time",
|
||||
help="Search by item start time of day, e.g. 12:00, or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--to-time",
|
||||
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
|
||||
type=TimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--year",
|
||||
metavar="YEAR",
|
||||
help="Search for items from a specific year, e.g. --year 2022 to find all photos from the year 2022. "
|
||||
"May be repeated to search multiple years.",
|
||||
multiple=True,
|
||||
type=int,
|
||||
),
|
||||
o(
|
||||
"--added-before",
|
||||
metavar="DATE",
|
||||
help="Search for items added to the library before a specific date/time, "
|
||||
"e.g. --added-before e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--added-after",
|
||||
metavar="DATE",
|
||||
help="Search for items added to the libray after a specific date/time, "
|
||||
"e.g. --added-after e.g. 2000-01-12T12:00:00, 2001-01-12T12:00:00-07:00, or 2000-12-31 (ISO 8601 with/without timezone).",
|
||||
type=DateTimeISO8601(),
|
||||
),
|
||||
o(
|
||||
"--added-in-last",
|
||||
metavar="TIME_DELTA",
|
||||
help="Search for items added to the library in the last TIME_DELTA, "
|
||||
"where TIME_DELTA is a string like "
|
||||
"'12 hrs', '1 day', '1d', '1 week', '2weeks', '1 month', '1 year'. "
|
||||
"for example, `--added-in-last 7d` and `--added-in-last '1 week'` are equivalent. "
|
||||
"months are assumed to be 30 days and years are assumed to be 365 days. "
|
||||
"Common English abbreviations are accepted, e.g. d, day, days or m, min, minutes.",
|
||||
type=TimeOffset(),
|
||||
),
|
||||
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
|
||||
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
|
||||
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
|
||||
o("--no-likes", is_flag=True, help="Search for photos with no likes."),
|
||||
o(
|
||||
"--is-reference",
|
||||
is_flag=True,
|
||||
help="Search for photos that were imported as referenced files (not copied into Photos library).",
|
||||
),
|
||||
o(
|
||||
"--not-reference",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not references, that is, they were copied into the Photos library "
|
||||
"and are managed by Photos.",
|
||||
),
|
||||
o(
|
||||
"--in-album",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in one or more albums.",
|
||||
),
|
||||
o(
|
||||
"--not-in-album",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in any albums.",
|
||||
),
|
||||
o(
|
||||
"--duplicate",
|
||||
is_flag=True,
|
||||
help="Search for photos with possible duplicates. osxphotos will compare signatures of photos, "
|
||||
"evaluating date created, size, height, width, and edited status to find *possible* duplicates. "
|
||||
"This does not compare images byte-for-byte nor compare hashes but should find photos imported multiple "
|
||||
"times or duplicated within Photos.",
|
||||
),
|
||||
o(
|
||||
"--min-size",
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size >= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
o(
|
||||
"--max-size",
|
||||
metavar="SIZE",
|
||||
type=BitMathSize(),
|
||||
help="Search for photos with size <= SIZE bytes. "
|
||||
"The size evaluated is the photo's original size (when imported to Photos). "
|
||||
"Size may be specified as integer bytes or using SI or NIST units. "
|
||||
"For example, the following are all valid and equivalent sizes: '1048576' '1.048576MB', '1 MiB'.",
|
||||
),
|
||||
o(
|
||||
"--regex",
|
||||
metavar="REGEX TEMPLATE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="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.",
|
||||
),
|
||||
o(
|
||||
"--selected",
|
||||
is_flag=True,
|
||||
help="Filter for photos that are currently selected in Photos.",
|
||||
),
|
||||
o(
|
||||
"--exif",
|
||||
metavar="EXIF_TAG VALUE",
|
||||
nargs=2,
|
||||
multiple=True,
|
||||
help="Search for photos where EXIF_TAG exists in photo's EXIF data and contains VALUE. "
|
||||
"For example, to find photos created by Adobe Photoshop: `--exif Software 'Adobe Photoshop' `"
|
||||
"or to find all photos shot on a Canon camera: `--exif Make Canon`. "
|
||||
"EXIF_TAG can be any valid exiftool tag, with or without group name, e.g. `EXIF:Make` or `Make`. "
|
||||
"To use --exif, exiftool must be installed and in the path.",
|
||||
),
|
||||
o(
|
||||
"--query-eval",
|
||||
metavar="CRITERIA",
|
||||
multiple=True,
|
||||
help="Evaluate CRITERIA to filter photos. "
|
||||
"CRITERIA will be evaluated in context of the following python list comprehension: "
|
||||
"`photos = [photo for photo in photos if CRITERIA]` "
|
||||
"where photo represents a PhotoInfo object. "
|
||||
"For example: `--query-eval photo.favorite` returns all photos that have been "
|
||||
"favorited and is equivalent to --favorite. "
|
||||
"You may specify more than one CRITERIA by using --query-eval multiple times. "
|
||||
"CRITERIA must be a valid python expression. "
|
||||
"See https://rhettbull.github.io/osxphotos/ for additional documentation on the PhotoInfo class.",
|
||||
),
|
||||
o(
|
||||
"--query-function",
|
||||
metavar="filename.py::function",
|
||||
multiple=True,
|
||||
type=FunctionCall(),
|
||||
help="Run function to filter photos. Use this in format: --query-function filename.py::function where filename.py is a python "
|
||||
+ "file you've created and function is the name of the function in the python file you want to call. "
|
||||
+ "Your function will be passed a list of PhotoInfo objects and is expected to return a filtered list of PhotoInfo objects. "
|
||||
+ "You may use more than one function by repeating the --query-function option with a different value. "
|
||||
+ "Your query function will be called after all other query options have been evaluated. "
|
||||
+ "See https://github.com/RhetTbull/osxphotos/blob/master/examples/query_function.py for example of how to use this option.",
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
def DEBUG_OPTIONS(f):
|
||||
o = click.option
|
||||
options = [
|
||||
o(
|
||||
"--debug",
|
||||
is_flag=True,
|
||||
help="Enable debug output.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
o(
|
||||
"--watch",
|
||||
metavar="FUNCTION_PATH",
|
||||
multiple=True,
|
||||
help="Watch function calls. For example, to watch all calls to FileUtil.copy: "
|
||||
"'--watch osxphotos.fileutil.FileUtil.copy'. More than one --watch option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
o(
|
||||
"--breakpoint",
|
||||
metavar="FUNCTION_PATH",
|
||||
multiple=True,
|
||||
help="Add breakpoint to function calls. For example, to add breakpoint to FileUtil.copy: "
|
||||
"'--breakpoint osxphotos.fileutil.FileUtil.copy'. More than one --breakpoint option can be specified.",
|
||||
hidden=OSXPHOTOS_HIDDEN,
|
||||
),
|
||||
]
|
||||
for o in options[::-1]:
|
||||
f = o(f)
|
||||
return f
|
||||
|
||||
|
||||
THEME_OPTION = click.option(
|
||||
"--theme",
|
||||
metavar="THEME",
|
||||
type=click.Choice(["dark", "light", "mono", "plain"], case_sensitive=False),
|
||||
help="Specify the color theme to use for --verbose output. "
|
||||
"Valid themes are 'dark', 'light', 'mono', and 'plain'. "
|
||||
"Defaults to 'dark' or 'light' depending on system dark mode setting.",
|
||||
)
|
||||
|
||||
|
||||
def load_uuid_from_file(filename):
|
||||
"""Load UUIDs from file. Does not validate UUIDs.
|
||||
Format is 1 UUID per line, any line beginning with # is ignored.
|
||||
Whitespace is stripped.
|
||||
|
||||
Arguments:
|
||||
filename: file name of the file containing UUIDs
|
||||
|
||||
Returns:
|
||||
list of UUIDs or empty list of no UUIDs in file
|
||||
|
||||
Raises:
|
||||
FileNotFoundError if file does not exist
|
||||
"""
|
||||
|
||||
if not pathlib.Path(filename).is_file():
|
||||
raise FileNotFoundError(f"Could not find file {filename}")
|
||||
|
||||
uuid = []
|
||||
with open(filename, "r") as uuid_file:
|
||||
for line in uuid_file:
|
||||
line = line.strip()
|
||||
if len(line) and line[0] != "#":
|
||||
uuid.append(line)
|
||||
return uuid
|
||||
|
||||
|
||||
def get_config_dir() -> pathlib.Path:
|
||||
"""Get the directory where config files are stored; create it if necessary."""
|
||||
config_dir = xdg_config_home() / APP_NAME
|
||||
|
||||
@@ -8,8 +8,17 @@ from rich import print
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||
from osxphotos.queryoptions import query_options_from_kwargs
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
JSON_OPTION,
|
||||
QUERY_OPTIONS,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .common import OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .verbose import verbose_print
|
||||
|
||||
@@ -24,30 +33,35 @@ from .verbose import verbose_print
|
||||
+ "can also use albums, persons, keywords, photos to dump related attributes.",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
"--uuid",
|
||||
metavar="UUID",
|
||||
help="Use with '--dump photos' to dump only certain UUIDs. "
|
||||
"May be repeated to include multiple UUIDs.",
|
||||
multiple=True,
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
|
||||
"""Print out debug info"""
|
||||
def debug_dump(
|
||||
ctx, cli_obj, db, photos_library, dump, verbose_flag, timestamp, **kwargs
|
||||
):
|
||||
"""Print out debug info.
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db)
|
||||
When run with --dump photos, any of the query options can be used to limit the
|
||||
photos printed. For example, to print info on currently selected photos:
|
||||
|
||||
osxphotos debug-dump --dump photos --selected
|
||||
"""
|
||||
|
||||
verbose = verbose_print(verbose_flag, timestamp)
|
||||
db = get_photos_db(*photos_library, db, cli_obj.db if cli_obj else None)
|
||||
if db is None:
|
||||
click.echo(ctx.obj.group.commands["debug-dump"].get_help(ctx), err=True)
|
||||
click.echo("\n\nLocated the following Photos library databases: ", err=True)
|
||||
_list_libraries()
|
||||
return
|
||||
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
|
||||
start_t = time.perf_counter()
|
||||
print(f"Opening database: {db}")
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose_)
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db, verbose=verbose)
|
||||
stop_t = time.perf_counter()
|
||||
print(f"Done; took {(stop_t-start_t):.2f} seconds")
|
||||
|
||||
@@ -78,16 +92,14 @@ def debug_dump(ctx, cli_obj, db, photos_library, dump, uuid, verbose):
|
||||
print("_dbpersons_fullname:")
|
||||
pprint.pprint(photosdb._dbpersons_fullname)
|
||||
elif attr == "photos":
|
||||
if uuid:
|
||||
for uuid_ in uuid:
|
||||
print(f"_dbphotos['{uuid_}']:")
|
||||
try:
|
||||
pprint.pprint(photosdb._dbphotos[uuid_])
|
||||
except KeyError:
|
||||
print(f"Did not find uuid {uuid_} in _dbphotos")
|
||||
else:
|
||||
print("_dbphotos:")
|
||||
pprint.pprint(photosdb._dbphotos)
|
||||
photos = photosdb.query(options=query_options)
|
||||
uuid = [photo.uuid for photo in photos]
|
||||
for uuid_ in uuid:
|
||||
print(f"_dbphotos['{uuid_}']:")
|
||||
try:
|
||||
pprint.pprint(photosdb._dbphotos[uuid_])
|
||||
except KeyError:
|
||||
print(f"Did not find uuid {uuid_} in _dbphotos")
|
||||
else:
|
||||
try:
|
||||
val = getattr(photosdb, attr)
|
||||
|
||||
@@ -13,10 +13,10 @@ from osxphotos._version import __version__
|
||||
from .common import get_config_dir, get_data_dir
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.command(name="docs")
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def docs(ctx, cli_obj):
|
||||
def docs_command(ctx, cli_obj):
|
||||
"""Open osxphotos documentation in your browser."""
|
||||
|
||||
# first check if docs installed in old location in confir dir and if so, delete them
|
||||
|
||||
@@ -11,15 +11,15 @@ from osxphotos.cli.click_rich_echo import (
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
|
||||
from .color_themes import get_default_theme
|
||||
from .common import (
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
FIELD_OPTION,
|
||||
JSON_OPTION,
|
||||
get_photos_db,
|
||||
)
|
||||
from .color_themes import get_default_theme
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .print_photo_info import print_photo_fields, print_photo_info
|
||||
from .verbose import get_verbose_console
|
||||
@@ -56,7 +56,11 @@ def dump(
|
||||
photos_library,
|
||||
print_template,
|
||||
):
|
||||
"""Print list of all photos & associated info from the Photos library."""
|
||||
"""Print list of all photos & associated info from the Photos library.
|
||||
|
||||
NOTE: dump is DEPRECATED and will be removed in a future release.
|
||||
Use `osxphotos query` instead.
|
||||
"""
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
|
||||
@@ -17,18 +17,12 @@ from osxphotos.fileutil import FileUtil, FileUtilNoOp
|
||||
from osxphotos.photoexporter import ExportOptions, ExportResults, PhotoExporter
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .cli_params import DB_OPTION, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo, rich_echo_error
|
||||
from .common import get_photos_db
|
||||
from .export import export, render_and_validate_report
|
||||
from .param_types import ExportDBType, TemplateString
|
||||
from .report_writer import ReportWriterNoOp, report_writer_factory
|
||||
from .report_writer import ReportWriterNoOp, export_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
@@ -166,8 +160,8 @@ from .verbose import get_verbose_console, verbose_print
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
@@ -203,7 +197,7 @@ def exiftool(
|
||||
save_config,
|
||||
theme,
|
||||
timestamp,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
):
|
||||
"""Run exiftool on previously exported files to update metadata.
|
||||
|
||||
@@ -235,6 +229,7 @@ def exiftool(
|
||||
|
||||
# need to ensure --exiftool is true in the config options
|
||||
locals_["exiftool"] = True
|
||||
locals_["verbose"] = verbose_flag
|
||||
config = ConfigOptions(
|
||||
"export",
|
||||
locals_,
|
||||
@@ -249,14 +244,7 @@ def exiftool(
|
||||
"save_config",
|
||||
],
|
||||
)
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose_flag, timestamp, theme=theme)
|
||||
|
||||
# load config options from either file or export database
|
||||
# values already set in config will take precedence over any values
|
||||
@@ -269,26 +257,16 @@ def exiftool(
|
||||
f"[error]Error parsing {load_config} config file: {e.message}", err=True
|
||||
)
|
||||
sys.exit(1)
|
||||
verbose_(f"Loaded options from file [filepath]{load_config}")
|
||||
verbose(f"Loaded options from file [filepath]{load_config}")
|
||||
elif db_config:
|
||||
config = export_db_get_config(exportdb, config)
|
||||
verbose_("Loaded options from export database")
|
||||
verbose("Loaded options from export database")
|
||||
|
||||
# from here on out, use config.param_name instead of using the params passed into the function
|
||||
# as the values may have been updated from config file or database
|
||||
if load_config or db_config:
|
||||
# config file might have changed verbose
|
||||
color_theme = get_theme(config.theme)
|
||||
verbose_ = verbose_print(
|
||||
config.verbose,
|
||||
config.timestamp,
|
||||
rich=True,
|
||||
theme=color_theme,
|
||||
highlight=False,
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_timestamp(config.timestamp)
|
||||
verbose = verbose_print(config.verbose, config.timestamp, theme=theme)
|
||||
|
||||
# validate options
|
||||
if append and not report:
|
||||
@@ -298,14 +276,14 @@ def exiftool(
|
||||
config.db = get_photos_db(config.db)
|
||||
|
||||
if save_config:
|
||||
verbose_(f"Saving options to config file '[filepath]{save_config}'")
|
||||
verbose(f"Saving options to config file '[filepath]{save_config}'")
|
||||
config.write_to_file(save_config)
|
||||
|
||||
process_files(exportdb, export_dir, verbose=verbose_, options=config)
|
||||
process_files(exportdb, export_dir, verbose=verbose, options=config)
|
||||
|
||||
|
||||
def process_files(
|
||||
exportdb: str, export_dir: str, verbose: Callable, options: ConfigOptions
|
||||
exportdb: str, export_dir: str, verbose: Callable[..., None], options: ConfigOptions
|
||||
):
|
||||
"""Process files in the export database.
|
||||
|
||||
@@ -320,7 +298,7 @@ def process_files(
|
||||
report = render_and_validate_report(
|
||||
options.report, options.exiftool_path, export_dir
|
||||
)
|
||||
report_writer = report_writer_factory(report, options.append)
|
||||
report_writer = export_report_writer_factory(report, options.append)
|
||||
else:
|
||||
report_writer = ReportWriterNoOp()
|
||||
|
||||
@@ -362,6 +340,12 @@ def process_files(
|
||||
hardlink_ok = True
|
||||
verbose(f"Processing file [filepath]{file}[/] ([num]{count}/{total}[/num])")
|
||||
photo = photosdb.get_photo(uuid)
|
||||
if not photo:
|
||||
verbose(
|
||||
f"Could not find photo for [filepath]{file}[/] ([uuid]{uuid}[/])"
|
||||
)
|
||||
report_writer.write(ExportResults(missing=[file]))
|
||||
continue
|
||||
export_options = ExportOptions(
|
||||
description_template=options.description_template,
|
||||
dry_run=options.dry_run,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
from textwrap import dedent
|
||||
|
||||
import click
|
||||
from rich import print
|
||||
@@ -15,20 +16,33 @@ from osxphotos.export_db import (
|
||||
ExportDB,
|
||||
)
|
||||
from osxphotos.export_db_utils import (
|
||||
export_db_backup,
|
||||
export_db_check_signatures,
|
||||
export_db_get_errors,
|
||||
export_db_get_last_run,
|
||||
export_db_get_version,
|
||||
export_db_save_config_to_file,
|
||||
export_db_touch_files,
|
||||
export_db_update_signatures,
|
||||
export_db_vacuum,
|
||||
export_db_migrate_photos_library,
|
||||
export_db_get_last_library,
|
||||
)
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .export import render_and_validate_report
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import report_writer_factory
|
||||
from .verbose import verbose_print
|
||||
from .report_writer import export_report_writer_factory
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
|
||||
@click.command(name="exportdb")
|
||||
@@ -65,6 +79,16 @@ from .verbose import verbose_print
|
||||
nargs=1,
|
||||
help="Print information about FILE_PATH contained in the database.",
|
||||
)
|
||||
@click.option(
|
||||
"--errors",
|
||||
is_flag=True,
|
||||
help="Print list of files that had warnings/errors on export (from all runs).",
|
||||
)
|
||||
@click.option(
|
||||
"--last-errors",
|
||||
is_flag=True,
|
||||
help="Print list of files that had warnings/errors on last export run.",
|
||||
)
|
||||
@click.option(
|
||||
"--uuid-files",
|
||||
metavar="UUID",
|
||||
@@ -120,6 +144,15 @@ from .verbose import verbose_print
|
||||
metavar="SQL_STATEMENT",
|
||||
help="Execute SQL_STATEMENT against export database and print results.",
|
||||
)
|
||||
@click.option(
|
||||
"--migrate-photos-library",
|
||||
metavar="PHOTOS_LIBRARY",
|
||||
help="Migrate the export database to use the specified Photos library. "
|
||||
"Use this if you have moved your Photos library to a new location or computer and "
|
||||
"want to keep using the same export database. "
|
||||
"This will update the UUIDs in the export database to match the new Photos library.",
|
||||
type=click.Path(exists=True, file_okay=True, dir_okay=True),
|
||||
)
|
||||
@click.option(
|
||||
"--export-dir",
|
||||
help="Optional path to export directory (if not parent of export database).",
|
||||
@@ -131,11 +164,13 @@ from .verbose import verbose_print
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", is_flag=True, help="Print verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@THEME_OPTION
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
help="Run in dry-run mode (don't actually update files), e.g. for use with --update-signatures.",
|
||||
help="Run in dry-run mode (don't actually update files); for example, use with --update-signatures or --migrate-photos-library.",
|
||||
)
|
||||
@click.argument("export_db", metavar="EXPORT_DATABASE", type=click.Path(exists=True))
|
||||
def exportdb(
|
||||
@@ -145,11 +180,16 @@ def exportdb(
|
||||
export_db,
|
||||
export_dir,
|
||||
info,
|
||||
errors,
|
||||
last_errors,
|
||||
last_run,
|
||||
migrate,
|
||||
migrate_photos_library,
|
||||
report,
|
||||
save_config,
|
||||
sql,
|
||||
theme,
|
||||
timestamp,
|
||||
touch_file,
|
||||
update_signatures,
|
||||
uuid_files,
|
||||
@@ -157,17 +197,16 @@ def exportdb(
|
||||
delete_uuid,
|
||||
delete_file,
|
||||
vacuum,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
version,
|
||||
):
|
||||
"""Utilities for working with the osxphotos export database"""
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
# validate options and args
|
||||
if append and not report:
|
||||
print(
|
||||
"[red]Error: --append requires --report; ee --help for more information.[/]",
|
||||
rich_echo_error(
|
||||
"[error]Error: --append requires --report; see --help for more information.[/]",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -177,8 +216,8 @@ def exportdb(
|
||||
# assume it's the export folder
|
||||
export_db = export_db / OSXPHOTOS_EXPORT_DB
|
||||
if not export_db.is_file():
|
||||
print(
|
||||
f"[red]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/red]"
|
||||
rich_echo_error(
|
||||
f"[error]Error: {OSXPHOTOS_EXPORT_DB} missing from {export_db.parent}[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
@@ -203,7 +242,9 @@ def exportdb(
|
||||
]
|
||||
]
|
||||
if sum(sub_commands) > 1:
|
||||
print("[red]Only a single sub-command may be specified at a time[/red]")
|
||||
rich_echo_error(
|
||||
"[error]Only a single sub-command may be specified at a time[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# process sub-commands
|
||||
@@ -212,11 +253,13 @@ def exportdb(
|
||||
try:
|
||||
osxphotos_ver, export_db_ver = export_db_get_version(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: could not read version from {export_db}: {e}[/red]")
|
||||
rich_echo_error(
|
||||
f"[error]Error: could not read version from {export_db}: {e}[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"osxphotos version: {osxphotos_ver}, export database version: {export_db_ver}"
|
||||
rich_echo(
|
||||
f"osxphotos version: [num]{osxphotos_ver}[/], export database version: [num]{export_db_ver}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -225,72 +268,76 @@ def exportdb(
|
||||
start_size = pathlib.Path(export_db).stat().st_size
|
||||
export_db_vacuum(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Vacuumed {export_db}! {start_size} bytes -> {pathlib.Path(export_db).stat().st_size} bytes"
|
||||
rich_echo(
|
||||
f"Vacuumed {export_db}! [num]{start_size}[/] bytes -> [num]{pathlib.Path(export_db).stat().st_size}[/] bytes"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if update_signatures:
|
||||
try:
|
||||
updated, skipped = export_db_update_signatures(
|
||||
export_db, export_dir, verbose_, dry_run
|
||||
export_db, export_dir, verbose, dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Done. Updated {updated} files, skipped {skipped} files.")
|
||||
rich_echo(
|
||||
f"Done. Updated [num]{updated}[/] files, skipped [num]{skipped}[/] files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if last_run:
|
||||
try:
|
||||
last_run_info = export_db_get_last_run(export_db)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"last run at {last_run_info[0]}:")
|
||||
print(f"osxphotos {last_run_info[1]}")
|
||||
rich_echo(f"last run at [time]{last_run_info[0]}:")
|
||||
rich_echo(f"osxphotos {last_run_info[1]}")
|
||||
sys.exit(0)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
export_db_save_config_to_file(export_db, save_config)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f"Saved configuration to {save_config}")
|
||||
rich_echo(f"Saved configuration to [filepath]{save_config}")
|
||||
sys.exit(0)
|
||||
|
||||
if check_signatures:
|
||||
try:
|
||||
matched, notmatched, skipped = export_db_check_signatures(
|
||||
export_db, export_dir, verbose_=verbose_
|
||||
export_db, export_dir, verbose_=verbose
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Done. Found {matched} matching signatures and {notmatched} signatures that don't match. Skipped {skipped} missing files."
|
||||
rich_echo(
|
||||
f"Done. Found [num]{matched}[/] matching signatures and [num]{notmatched}[/] signatures that don't match. "
|
||||
f"Skipped [num]{skipped}[/] missing files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if touch_file:
|
||||
try:
|
||||
touched, not_touched, skipped = export_db_touch_files(
|
||||
export_db, export_dir, verbose_=verbose_, dry_run=dry_run
|
||||
export_db, export_dir, verbose_=verbose, dry_run=dry_run
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(
|
||||
f"Done. Touched {touched} files, skipped {not_touched} up to date files, skipped {skipped} missing files."
|
||||
rich_echo(
|
||||
f"Done. Touched [num]{touched}[/] files, skipped [num]{not_touched}[/] up to date files, "
|
||||
f"skipped [num]{skipped}[/] missing files."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -299,28 +346,63 @@ def exportdb(
|
||||
try:
|
||||
info_rec = exportdb.get_file_record(info)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if info_rec:
|
||||
# use rich print as rich_echo doesn't highlight json
|
||||
print(info_rec.json(indent=2))
|
||||
else:
|
||||
print(f"[red]File '{info}' not found in export database[/red]")
|
||||
rich_echo(f"[error]File '{info}' not found in export database[/error]")
|
||||
sys.exit(0)
|
||||
|
||||
if errors:
|
||||
# list errors
|
||||
try:
|
||||
error_list = export_db_get_errors(export_db)
|
||||
except Exception as e:
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if error_list:
|
||||
for error in error_list:
|
||||
rich_echo(error)
|
||||
else:
|
||||
rich_echo("No errors found")
|
||||
sys.exit(0)
|
||||
|
||||
if last_errors:
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
if export_results := exportdb.get_export_results(0):
|
||||
for error in [
|
||||
*export_results.error,
|
||||
*export_results.exiftool_error,
|
||||
*export_results.exiftool_warning,
|
||||
]:
|
||||
rich_click_echo(
|
||||
f"[filepath]{error[0]}[/], [time]{export_results.datetime}[/], [error]{error[1]}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
else:
|
||||
rich_echo_error("[error]Results from last run not found in database[/]")
|
||||
sys.exit(1)
|
||||
|
||||
if uuid_info:
|
||||
# get photoinfo record for a uuid
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
try:
|
||||
info_rec = exportdb.get_photoinfo_for_uuid(uuid_info)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if info_rec:
|
||||
# use rich print as rich_echo doesn't highlight json
|
||||
print(json.dumps(json.loads(info_rec), sort_keys=True, indent=2))
|
||||
else:
|
||||
print(f"[red]UUID '{uuid_info}' not found in export database[/red]")
|
||||
rich_echo(
|
||||
f"[error]UUID '{uuid_info}' not found in export database[/error]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if uuid_files:
|
||||
@@ -329,32 +411,38 @@ def exportdb(
|
||||
try:
|
||||
file_list = exportdb.get_files_for_uuid(uuid_files)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
if file_list:
|
||||
for f in file_list:
|
||||
print(f)
|
||||
rich_echo(f"[filepath]{f}[/]")
|
||||
else:
|
||||
print(f"[red]UUID '{uuid_files}' not found in export database[/red]")
|
||||
rich_echo(
|
||||
f"[error]UUID '{uuid_files}' not found in export database[/error]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if delete_uuid:
|
||||
# delete a uuid from the export database
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
for uuid in delete_uuid:
|
||||
print(f"Deleting uuid {uuid} from database.")
|
||||
rich_echo(f"Deleting uuid [uuid]{uuid}[/] from database.")
|
||||
count = exportdb.delete_data_for_uuid(uuid)
|
||||
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
|
||||
rich_echo(
|
||||
f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if delete_file:
|
||||
# delete information associated with a file from the export database
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
for filepath in delete_file:
|
||||
print(f"Deleting file {filepath} from database.")
|
||||
rich_echo(f"Deleting file [filepath]{filepath}[/] from database.")
|
||||
count = exportdb.delete_data_for_filepath(filepath)
|
||||
print(f"Deleted {count} {pluralize(count, 'record', 'records')}.")
|
||||
rich_echo(
|
||||
f"Deleted [num]{count}[/] {pluralize(count, 'record', 'records')}."
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
if report:
|
||||
@@ -363,27 +451,29 @@ def exportdb(
|
||||
report_filename = render_and_validate_report(report_template, "", export_dir)
|
||||
export_results = exportdb.get_export_results(run_id)
|
||||
if not export_results:
|
||||
print(f"[red]No report results found for run ID {run_id}[/red]")
|
||||
rich_echo_error(
|
||||
f"[error]No report results found for run ID {run_id}[/error]"
|
||||
)
|
||||
sys.exit(1)
|
||||
try:
|
||||
report_writer = report_writer_factory(report_filename, append=append)
|
||||
report_writer = export_report_writer_factory(report_filename, append=append)
|
||||
except ValueError as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
report_writer.write(export_results)
|
||||
report_writer.close()
|
||||
print(f"Wrote report to {report_filename}")
|
||||
rich_echo(f"Wrote report to [filepath]{report_filename}[/]")
|
||||
sys.exit(0)
|
||||
|
||||
if migrate:
|
||||
exportdb = ExportDB(export_db, export_dir)
|
||||
if upgraded := exportdb.was_upgraded:
|
||||
print(
|
||||
f"Migrated export database {export_db} from version {upgraded[0]} to {upgraded[1]}"
|
||||
rich_echo(
|
||||
f"Migrated export database [filepath]{export_db}[/] from version [num]{upgraded[0]}[/] to [num]{upgraded[1]}[/]"
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"Export database {export_db} is already at latest version {OSXPHOTOS_EXPORTDB_VERSION}"
|
||||
rich_echo(
|
||||
f"Export database [filepath]{export_db}[/] is already at latest version [num]{OSXPHOTOS_EXPORTDB_VERSION}[/]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -393,9 +483,39 @@ def exportdb(
|
||||
c = exportdb._conn.cursor()
|
||||
results = c.execute(sql)
|
||||
except Exception as e:
|
||||
print(f"[red]Error: {e}[/red]")
|
||||
rich_echo_error(f"[error]Error: {e}[/error]")
|
||||
sys.exit(1)
|
||||
else:
|
||||
for row in results:
|
||||
print(row)
|
||||
sys.exit(0)
|
||||
|
||||
if migrate_photos_library:
|
||||
# migrate Photos library to new library and update UUIDs in export database
|
||||
last_library = export_db_get_last_library(export_db)
|
||||
rich_echo(
|
||||
dedent(
|
||||
f"""
|
||||
[warning]:warning-emoji: This command will update your export database ([filepath]{export_db}[/])
|
||||
to use [filepath]{migrate_photos_library}[/] as the new source library.
|
||||
The last library used was [filepath]{last_library}[/].
|
||||
This will allow you to use the export database with the new library but it will
|
||||
no longer work correctly with the old library unless you run the `--migrate-photos-library`
|
||||
command again to update the export database to use the previous library.
|
||||
|
||||
A backup of the export database will be created in the same directory as the export database.
|
||||
"""
|
||||
)
|
||||
)
|
||||
if not click.confirm("Do you want to continue?"):
|
||||
sys.exit(0)
|
||||
if not dry_run:
|
||||
backup_file = export_db_backup(export_db)
|
||||
verbose(f"Backed up export database to [filepath]{backup_file}[/]")
|
||||
migrated, notmigrated = export_db_migrate_photos_library(
|
||||
export_db, migrate_photos_library, verbose, dry_run
|
||||
)
|
||||
rich_echo(
|
||||
f"Migrated [num]{migrated}[/] {pluralize(migrated, 'photo', 'photos')}, "
|
||||
f"[num]{notmigrated}[/] not migrated."
|
||||
)
|
||||
|
||||
@@ -8,7 +8,8 @@ from rich import print
|
||||
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
|
||||
from osxphotos.sqlgrep import sqlgrep
|
||||
|
||||
from .common import DB_OPTION, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .cli_params import DB_OPTION, OSXPHOTOS_HIDDEN
|
||||
from .common import get_photos_db
|
||||
|
||||
|
||||
@click.command(name="grep", hidden=OSXPHOTOS_HIDDEN)
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"""import command for osxphotos CLI to import photos into Photos"""
|
||||
|
||||
# Note: the style in this module is a bit different than much of the other osxphotos code
|
||||
# As an experiment, I've used mostly functions instead of classes (e.g. the report writer
|
||||
# functions vs ReportWriter class used by export) and I've kept everything for import
|
||||
# self-contained in this one file
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
@@ -30,6 +27,7 @@ from strpdatetime import strpdatetime
|
||||
|
||||
from osxphotos._constants import _OSXPHOTOS_NONE_SENTINEL
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.cli.cli_params import TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from osxphotos.cli.common import get_data_dir
|
||||
from osxphotos.cli.help import HELP_WIDTH
|
||||
from osxphotos.cli.param_types import FunctionCall, StrpDateTimePattern, TemplateString
|
||||
@@ -47,18 +45,17 @@ from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import THEME_OPTION
|
||||
from .cli_params import THEME_OPTION
|
||||
from .click_rich_echo import rich_click_echo, rich_echo_error
|
||||
from .rich_progress import rich_progress
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
# Note: the style in this module is a bit different than much of the other osxphotos code
|
||||
# As an experiment, I've used mostly functions instead of classes (e.g. the report writer
|
||||
# functions vs ReportWriter class used by export) and I've kept everything for import
|
||||
# self-contained in this one file
|
||||
|
||||
|
||||
MetaData = namedtuple("MetaData", ["title", "description", "keywords", "location"])
|
||||
|
||||
OSXPHOTOS_ABOUT_STRING = f"Created by osxphotos version {__version__} (https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||
@@ -165,7 +162,8 @@ def import_photo(
|
||||
Args:
|
||||
filepath: path to the file to import
|
||||
dup_check: enable or disable Photo's duplicate check on import
|
||||
verbose: Callable"""
|
||||
verbose: Callable
|
||||
"""
|
||||
if imported := PhotosLibrary().import_photos(
|
||||
[filepath], skip_duplicate_check=not dup_check
|
||||
):
|
||||
@@ -207,7 +205,7 @@ def add_photo_to_albums(
|
||||
split_folder: str,
|
||||
exiftool_path: Path,
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> list[str]:
|
||||
"""Add photo to one or more albums"""
|
||||
albums = []
|
||||
for a in album:
|
||||
@@ -225,6 +223,7 @@ def add_photo_to_albums(
|
||||
a, verbose=verbose, split_folder=split_folder, rich=True
|
||||
)
|
||||
photos_album.add(photo)
|
||||
return albums
|
||||
|
||||
|
||||
def clear_photo_metadata(photo: Photo, filepath: Path, verbose: Callable[..., None]):
|
||||
@@ -319,6 +318,8 @@ def location_from_file(
|
||||
latitude = -latitude
|
||||
elif latitude_ref != "N":
|
||||
latitude = None
|
||||
if latitude is None:
|
||||
latitude = metadata.get("XMP:GPSLatitude")
|
||||
if longitude := metadata.get("EXIF:GPSLongitude"):
|
||||
longitude = float(longitude)
|
||||
longitude_ref = metadata.get("EXIF:GPSLongitudeRef")
|
||||
@@ -326,6 +327,8 @@ def location_from_file(
|
||||
longitude = -longitude
|
||||
elif longitude_ref != "E":
|
||||
longitude = None
|
||||
if longitude is None:
|
||||
longitude = metadata.get("XMP:GPSLongitude")
|
||||
if latitude is None or longitude is None:
|
||||
# maybe it's a video
|
||||
if lat_lon := metadata.get("QuickTime:GPSCoordinates") or metadata.get(
|
||||
@@ -344,7 +347,11 @@ def location_from_file(
|
||||
return latitude, longitude
|
||||
|
||||
|
||||
def set_photo_metadata(photo: Photo, metadata: MetaData, merge_keywords: bool):
|
||||
def set_photo_metadata(
|
||||
photo: Photo,
|
||||
metadata: MetaData,
|
||||
merge_keywords: bool,
|
||||
) -> MetaData:
|
||||
"""Set metadata (title, description, keywords) for a Photo object"""
|
||||
photo.title = metadata.title
|
||||
photo.description = metadata.description
|
||||
@@ -354,6 +361,7 @@ def set_photo_metadata(photo: Photo, metadata: MetaData, merge_keywords: bool):
|
||||
keywords.extend(old_keywords)
|
||||
keywords = list(set(keywords))
|
||||
photo.keywords = keywords
|
||||
return MetaData(metadata.title, metadata.description, keywords, metadata.location)
|
||||
|
||||
|
||||
def set_photo_metadata_from_exiftool(
|
||||
@@ -362,12 +370,12 @@ def set_photo_metadata_from_exiftool(
|
||||
exiftool_path: str,
|
||||
merge_keywords: bool,
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> MetaData:
|
||||
"""Set photo's metadata by reading metadata form file with exiftool"""
|
||||
verbose(f"Setting metadata and location from EXIF for [filename]{filepath.name}[/]")
|
||||
metadata = metadata_from_file(filepath, exiftool_path)
|
||||
if any([metadata.title, metadata.description, metadata.keywords]):
|
||||
set_photo_metadata(photo, metadata, merge_keywords)
|
||||
metadata = set_photo_metadata(photo, metadata, merge_keywords)
|
||||
verbose(f"Set metadata for [filename]{filepath.name}[/]:")
|
||||
verbose(
|
||||
f"title='{metadata.title}', description='{metadata.description}', keywords={metadata.keywords}"
|
||||
@@ -383,6 +391,7 @@ def set_photo_metadata_from_exiftool(
|
||||
)
|
||||
else:
|
||||
verbose(f"No location to set for [filename]{filepath.name}[/]")
|
||||
return metadata
|
||||
|
||||
|
||||
def set_photo_title(
|
||||
@@ -392,7 +401,7 @@ def set_photo_title(
|
||||
title_template: str,
|
||||
exiftool_path: str,
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> str:
|
||||
"""Set title of photo"""
|
||||
title_text = render_photo_template(
|
||||
filepath, relative_filepath, title_template, exiftool_path
|
||||
@@ -408,6 +417,9 @@ def set_photo_title(
|
||||
f"Setting title of photo [filename]{filepath.name}[/] to '{title_text[0]}'"
|
||||
)
|
||||
photo.title = title_text[0]
|
||||
return title_text[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def set_photo_description(
|
||||
@@ -417,7 +429,7 @@ def set_photo_description(
|
||||
description_template: str,
|
||||
exiftool_path: str,
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> str:
|
||||
"""Set description of photo"""
|
||||
description_text = render_photo_template(
|
||||
filepath, relative_filepath, description_template, exiftool_path
|
||||
@@ -433,6 +445,9 @@ def set_photo_description(
|
||||
f"Setting description of photo [filename]{filepath.name}[/] to '{description_text[0]}'"
|
||||
)
|
||||
photo.description = description_text[0]
|
||||
return description_text[0]
|
||||
else:
|
||||
return ""
|
||||
|
||||
|
||||
def set_photo_keywords(
|
||||
@@ -443,7 +458,7 @@ def set_photo_keywords(
|
||||
exiftool_path: str,
|
||||
merge: bool,
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> list[str]:
|
||||
"""Set keywords of photo"""
|
||||
keywords = []
|
||||
for keyword in keyword_template:
|
||||
@@ -456,6 +471,7 @@ def set_photo_keywords(
|
||||
keywords = list(set(keywords))
|
||||
verbose(f"Setting keywords of photo [filename]{filepath.name}[/] to {keywords}")
|
||||
photo.keywords = keywords
|
||||
return keywords
|
||||
|
||||
|
||||
def set_photo_location(
|
||||
@@ -463,17 +479,18 @@ def set_photo_location(
|
||||
filepath: Path,
|
||||
location: Tuple[float, float],
|
||||
verbose: Callable[..., None],
|
||||
):
|
||||
) -> tuple[float, float]:
|
||||
"""Set location of photo"""
|
||||
verbose(
|
||||
f"Setting location of photo [filename]{filepath.name}[/] to {location[0]}, {location[1]}"
|
||||
)
|
||||
photo.location = location
|
||||
return location
|
||||
|
||||
|
||||
def set_photo_date_from_filename(
|
||||
photo: Photo, filepath: Path, parse_date: str, verbose: Callable[..., None]
|
||||
):
|
||||
) -> datetime.datetime | None:
|
||||
"""Set date of photo from filename"""
|
||||
# TODO: handle timezone (use code from timewarp), for now convert timezone to local timezone
|
||||
try:
|
||||
@@ -491,11 +508,12 @@ def set_photo_date_from_filename(
|
||||
verbose(
|
||||
f"[warning]Could not parse date from filename [filename]{filepath.name}[/][/]"
|
||||
)
|
||||
return
|
||||
return None
|
||||
verbose(
|
||||
f"Setting date of photo [filename]{filepath.name}[/] to [time]{date.strftime('%Y-%m-%d %H:%M:%S')}[/]"
|
||||
)
|
||||
photo.date = date
|
||||
return date
|
||||
|
||||
|
||||
def get_relative_filepath(filepath: Path, relative_to: Optional[str]) -> Path:
|
||||
@@ -588,6 +606,8 @@ def check_templates_and_exit(
|
||||
|
||||
@dataclass
|
||||
class ReportRecord:
|
||||
"""Dataclass that records metadata on each file imported for writing to report"""
|
||||
|
||||
albums: List[str] = field(default_factory=list)
|
||||
description: str = ""
|
||||
error: bool = False
|
||||
@@ -615,6 +635,13 @@ class ReportRecord:
|
||||
)
|
||||
return cls(**dict_data)
|
||||
|
||||
def update_from_metadata(self, metadata: MetaData):
|
||||
"""Update a ReportRecord with data from a MetaData"""
|
||||
self.title = metadata.title
|
||||
self.description = metadata.description
|
||||
self.keywords = metadata.keywords
|
||||
self.location = metadata.location
|
||||
|
||||
def asdict(self):
|
||||
return asdict(self)
|
||||
|
||||
@@ -628,15 +655,15 @@ class ReportRecord:
|
||||
|
||||
def update_report_record(report_record: ReportRecord, photo: Photo, filepath: Path):
|
||||
"""Update a ReportRecord with data from a Photo"""
|
||||
report_record.albums = [a.title for a in photo.albums]
|
||||
report_record.description = photo.description
|
||||
|
||||
# do not update albums as they are added to the report record as they are imported (#934)
|
||||
report_record.filename = filepath.name
|
||||
report_record.filepath = filepath
|
||||
report_record.imported = True
|
||||
report_record.uuid = photo.uuid
|
||||
report_record.title = photo.title
|
||||
report_record.description = photo.description
|
||||
report_record.keywords = photo.keywords
|
||||
report_record.location = photo.location
|
||||
report_record.title = photo.title
|
||||
report_record.uuid = photo.uuid
|
||||
|
||||
return report_record
|
||||
|
||||
@@ -1329,10 +1356,8 @@ class ImportCommand(click.Command):
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose_", is_flag=True, help="Print verbose output.")
|
||||
@click.option(
|
||||
"--timestamp", "-T", is_flag=True, help="Add time stamp to verbose output"
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--no-progress", is_flag=True, help="Do not display progress bar during import."
|
||||
)
|
||||
@@ -1386,19 +1411,12 @@ def import_cli(
|
||||
theme,
|
||||
timestamp,
|
||||
title,
|
||||
verbose_,
|
||||
verbose_flag,
|
||||
walk,
|
||||
):
|
||||
"""Import photos and videos into Photos."""
|
||||
|
||||
color_theme = get_theme(theme)
|
||||
verbose = verbose_print(
|
||||
verbose_, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if not files:
|
||||
echo("Nothing to import", err=True)
|
||||
@@ -1473,11 +1491,13 @@ def import_cli(
|
||||
report_data[filepath] = ReportRecord(
|
||||
filepath=filepath, filename=filepath.name
|
||||
)
|
||||
report_record = report_data[filepath]
|
||||
photo, error = import_photo(filepath, dup_check, verbose)
|
||||
if error:
|
||||
error_count += 1
|
||||
report_data[filepath].error = True
|
||||
report_record.error = True
|
||||
continue
|
||||
report_record.imported = True
|
||||
imported_count += 1
|
||||
|
||||
if clear_metadata:
|
||||
@@ -1488,12 +1508,21 @@ def import_cli(
|
||||
|
||||
if exiftool:
|
||||
set_photo_metadata_from_exiftool(
|
||||
photo, filepath, exiftool_path, merge_keywords, verbose
|
||||
photo,
|
||||
filepath,
|
||||
exiftool_path,
|
||||
merge_keywords,
|
||||
verbose,
|
||||
)
|
||||
|
||||
if title:
|
||||
set_photo_title(
|
||||
photo, filepath, relative_filepath, title, exiftool_path, verbose
|
||||
photo,
|
||||
filepath,
|
||||
relative_filepath,
|
||||
title,
|
||||
exiftool_path,
|
||||
verbose,
|
||||
)
|
||||
|
||||
if description:
|
||||
@@ -1522,9 +1551,10 @@ def import_cli(
|
||||
|
||||
if parse_date:
|
||||
set_photo_date_from_filename(photo, filepath, parse_date, verbose)
|
||||
# TODO: ReportRecord doesn't currently record date
|
||||
|
||||
if album:
|
||||
add_photo_to_albums(
|
||||
report_record.albums = add_photo_to_albums(
|
||||
photo,
|
||||
filepath,
|
||||
relative_filepath,
|
||||
@@ -1539,14 +1569,15 @@ def import_cli(
|
||||
# post function is tuple of (function, filename.py::function_name)
|
||||
verbose(f"Calling post-function [bold]{function[1]}")
|
||||
try:
|
||||
function[0](photo, filepath, verbose)
|
||||
function[0](photo, filepath, verbose, report_record)
|
||||
except Exception as e:
|
||||
rich_echo_error(
|
||||
f"[error]Error running post-function [italic]{function[1]}[/italic]: {e}"
|
||||
)
|
||||
|
||||
update_report_record(report_data[filepath], photo, filepath)
|
||||
import_db.set(str(filepath), report_data[filepath])
|
||||
# update report data
|
||||
update_report_record(report_record, photo, filepath)
|
||||
import_db.set(str(filepath), report_record)
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ import yaml
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""install/uninstall/run commands for osxphotos CLI"""
|
||||
|
||||
|
||||
import contextlib
|
||||
import sys
|
||||
from runpy import run_module, run_path
|
||||
|
||||
@@ -50,14 +52,19 @@ def uninstall(packages, yes):
|
||||
|
||||
|
||||
@click.command(name="run", cls=RunCommand)
|
||||
# help command passed just to keep click from intercepting help
|
||||
# and allowing --help to be passed to the script being run
|
||||
@click.option("--help", "-h", is_flag=True, help="Show this message and exit")
|
||||
@click.argument("python_file", nargs=1, type=click.Path(exists=True))
|
||||
@click.argument("args", metavar="ARGS", nargs=-1)
|
||||
def run(python_file, help, args):
|
||||
"""Run a python file using same environment as osxphotos.
|
||||
Any args are made available to the python file."""
|
||||
# drop first two arguments, which are the osxphotos script and run command
|
||||
sys.argv = sys.argv[2:]
|
||||
|
||||
# Need to drop all the args from sys.argv up to and including the run command
|
||||
# For example, command could be one of the following:
|
||||
# osxphotos run example.py --help
|
||||
# osxphotos --debug run example.py --verbose --db /path/to/photos.db
|
||||
# etc.
|
||||
with contextlib.suppress(ValueError):
|
||||
index = sys.argv.index("run")
|
||||
sys.argv = sys.argv[index + 1 :]
|
||||
run_path(python_file, run_name="__main__")
|
||||
|
||||
@@ -7,7 +7,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
50
osxphotos/cli/kvstore.py
Normal file
50
osxphotos/cli/kvstore.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""Simple interface to SQLiteKVStore for storing state between runs of the CLI tool."""
|
||||
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import contextlib
|
||||
import datetime
|
||||
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
|
||||
from .common import get_data_dir
|
||||
|
||||
__all__ = ["kvstore"]
|
||||
|
||||
# Store open connections
|
||||
__kvstores = []
|
||||
|
||||
|
||||
@atexit.register
|
||||
def close_kvstore():
|
||||
"""Close any open SQLiteKVStore databases"""
|
||||
global __kvstores
|
||||
for kv in __kvstores:
|
||||
with contextlib.suppress(Exception):
|
||||
kv.close()
|
||||
|
||||
|
||||
def kvstore(name: str) -> SQLiteKVStore:
|
||||
"""Return a key/value store for storing state between commands.
|
||||
|
||||
The key/value store is a SQLite database stored in the user's XDG data directory,
|
||||
usually `~/.local/share/`. The key/value store can be used like a dict to store
|
||||
arbitrary key/value pairs which persist between runs of the CLI tool.
|
||||
|
||||
Args:
|
||||
name: a unique name for the key/value store
|
||||
|
||||
Returns:
|
||||
SQLiteKVStore object
|
||||
"""
|
||||
global __kvstores
|
||||
data_dir = get_data_dir()
|
||||
if not name.endswith(".db"):
|
||||
name += ".db"
|
||||
kv = SQLiteKVStore(str(data_dir / name), wal=True)
|
||||
if not kv.about:
|
||||
kv.about = f"Key/value store for {name}, created by osxphotos CLI on {datetime.datetime.now()}"
|
||||
__kvstores.append(kv)
|
||||
return kv
|
||||
@@ -7,7 +7,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import click
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import JSON_OPTION
|
||||
from .cli_params import JSON_OPTION
|
||||
|
||||
|
||||
@click.command(name="list")
|
||||
|
||||
@@ -14,17 +14,16 @@ from typing import Dict
|
||||
import click
|
||||
|
||||
from osxphotos import PhotosDB
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, UUID_PATTERN
|
||||
from osxphotos.fileutil import FileUtil
|
||||
from osxphotos.utils import increment_filename, pluralize
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import set_rich_console, set_rich_theme, set_rich_timestamp
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .common import get_photos_db
|
||||
from .help import get_help_msg
|
||||
from .list import _list_libraries
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
from .verbose import verbose_print
|
||||
|
||||
|
||||
@click.command(name="orphans")
|
||||
@@ -36,22 +35,15 @@ from .verbose import get_verbose_console, verbose_print
|
||||
help="Export orphans to directory EXPORT_PATH. If --export not specified, orphans are listed but not exported.",
|
||||
)
|
||||
@DB_OPTION
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def orphans(ctx, cli_obj, export, db, verbose, timestamp, theme):
|
||||
def orphans(ctx, cli_obj, export, db, verbose_flag, timestamp, theme):
|
||||
"""Find orphaned photos in a Photos library"""
|
||||
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose, timestamp, rich=True, theme=color_theme, highlight=False
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
verbose_ = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
@@ -139,8 +131,7 @@ def scan_for_files(directory: str, uuid_dict: Dict):
|
||||
|
||||
Note: modifies uuid_dict
|
||||
"""
|
||||
uuid_pattern = r"([0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})"
|
||||
uuid_regex = re.compile(uuid_pattern)
|
||||
uuid_regex = re.compile(UUID_PATTERN)
|
||||
for dirpath, dirname, filenames in os.walk(directory):
|
||||
for filename in filenames:
|
||||
if match := uuid_regex.match(filename):
|
||||
|
||||
@@ -20,8 +20,10 @@ __all__ = [
|
||||
"BitMathSize",
|
||||
"DateOffset",
|
||||
"DateTimeISO8601",
|
||||
"DeprecatedPath",
|
||||
"ExportDBType",
|
||||
"FunctionCall",
|
||||
"PathOrStdin",
|
||||
"StrpDateTimePattern",
|
||||
"TemplateString",
|
||||
"TimeISO8601",
|
||||
@@ -31,6 +33,38 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class DeprecatedPath(click.Path):
|
||||
"""A click.Path that prints a deprecation warning when used."""
|
||||
|
||||
name = "DEPRECATED_PATH"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if "deprecation_warning" in kwargs:
|
||||
self.deprecation_warning = kwargs.pop("deprecation_warning")
|
||||
else:
|
||||
self.deprecation_warning = "This option is deprecated and will be removed in a future version of osxphotos."
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
click.echo(
|
||||
f"WARNING: {param.name} is deprecated. {self.deprecation_warning}",
|
||||
err=True,
|
||||
)
|
||||
return super().convert(value, param, ctx)
|
||||
|
||||
|
||||
class PathOrStdin(click.Path):
|
||||
"""A click.Path or "-" to represent STDIN."""
|
||||
|
||||
name = "PATH_OR_STDIN"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
return value if value == "-" else super().convert(value, param, ctx)
|
||||
|
||||
|
||||
class DateTimeISO8601(click.ParamType):
|
||||
|
||||
name = "DATETIME"
|
||||
|
||||
@@ -6,7 +6,8 @@ import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
"""Inspect photos selected in Photos """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import pathlib
|
||||
import re
|
||||
from fractions import Fraction
|
||||
from multiprocessing import Process, Queue
|
||||
from queue import Empty
|
||||
from time import gmtime, sleep, strftime
|
||||
from typing import List, Optional, Tuple
|
||||
import pathlib
|
||||
from typing import Generator, List, Optional, Tuple
|
||||
|
||||
import bitmath
|
||||
import click
|
||||
@@ -19,13 +21,14 @@ from rich.live import Live
|
||||
from rich.panel import Panel
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB
|
||||
from osxphotos._constants import _UNKNOWN_PERSON
|
||||
from osxphotos._constants import _UNKNOWN_PERSON, search_category_factory
|
||||
from osxphotos.rich_utils import add_rich_markup_tag
|
||||
from osxphotos.text_detection import detect_text as detect_text_in_photo
|
||||
from osxphotos.utils import dd_to_dms_str
|
||||
|
||||
from .cli_params import DB_OPTION, THEME_OPTION
|
||||
from .color_themes import get_theme
|
||||
from .common import DB_OPTION, THEME_OPTION, get_photos_db
|
||||
from .common import get_photos_db
|
||||
|
||||
# global that tracks UUID being inspected
|
||||
CURRENT_UUID = None
|
||||
@@ -35,6 +38,17 @@ bold = add_rich_markup_tag("bold")
|
||||
dim = add_rich_markup_tag("dim")
|
||||
|
||||
|
||||
def add_cyclic_color_tag(values: list[str]) -> Generator[str, None, None]:
|
||||
"""Add a rich markup tag to each str in values, cycling through a set of colors"""
|
||||
# reuse some colors already in the theme
|
||||
# these are chosen for contrast to easily associate scores and values
|
||||
colors = ["change", "count", "filepath", "filename"]
|
||||
color_tags = [add_rich_markup_tag(color) for color in colors]
|
||||
modidx = len(color_tags)
|
||||
for idx, val in enumerate(values):
|
||||
yield color_tags[idx % modidx](val)
|
||||
|
||||
|
||||
def extract_uuid(text: str) -> str:
|
||||
"""Extract a UUID from a string"""
|
||||
if match := re.search(
|
||||
@@ -52,6 +66,22 @@ def trim(text: str, pad: str = "") -> str:
|
||||
return text if len(text) <= width else f"{text[: width- 3]}..."
|
||||
|
||||
|
||||
def format_search_info(photo: PhotoInfo) -> str:
|
||||
"""Format search info for photo"""
|
||||
categories = sorted(list(photo._db._db_searchinfo_categories.keys()))
|
||||
search_info = photo.search_info
|
||||
if not search_info:
|
||||
return ""
|
||||
search_info_strs = []
|
||||
category_dict = search_category_factory(photo._db.photos_version).categories()
|
||||
for category in categories:
|
||||
if text := search_info._get_text_for_category(category):
|
||||
text = ", ".join(t for t in text if t) if isinstance(text, list) else text
|
||||
category_name = str(category_dict.get(category, category)).lower()
|
||||
search_info_strs.append(f"{bold(category_name)}: {text}")
|
||||
return ", ".join(search_info_strs)
|
||||
|
||||
|
||||
def inspect_photo(
|
||||
photo: PhotoInfo,
|
||||
detected_text: Optional[str] = None,
|
||||
@@ -125,8 +155,10 @@ def inspect_photo(
|
||||
+ f"{', '.join(dd_to_dms_str(*photo.location)) if photo.location[0] else '-'}",
|
||||
bold("Place: ") + f"{photo.place.name if photo.place else '-'}",
|
||||
bold("Categories/Labels: ") + f"{', '.join(photo.labels) or '-'}",
|
||||
bold("Search Info: ") + format_search_info(photo),
|
||||
]
|
||||
)
|
||||
|
||||
properties.append(format_flags(photo))
|
||||
properties.append(format_albums(photo))
|
||||
|
||||
@@ -191,9 +223,15 @@ def format_templates(photo: PhotoInfo, templates: List[str]) -> str:
|
||||
|
||||
def format_score_info(photo: PhotoInfo) -> str:
|
||||
"""Format score_info"""
|
||||
score_str = bold("Score: ")
|
||||
score_str = bold("Scores: ")
|
||||
if photo.score:
|
||||
score_str += f"[num]{photo.score.overall}[/]" if photo.score else "-"
|
||||
# add color tags to each key: value pair to easily associate keys/values
|
||||
score_values = add_cyclic_color_tag(
|
||||
[f"{k}: {float(v):.2f}" for k, v in photo.score.asdict().items()]
|
||||
)
|
||||
score_str += ", ".join(score_values)
|
||||
else:
|
||||
score_str += "-"
|
||||
return score_str
|
||||
|
||||
|
||||
@@ -245,14 +283,18 @@ def format_paths(photo: PhotoInfo) -> str:
|
||||
"""format photo paths for inspect_photo"""
|
||||
path_str = bold("Path original: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path)}[/]" if photo.path else "-"
|
||||
if photo.path_edited:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
|
||||
if photo.path_live_photo:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path live video: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_live_photo)}[/]"
|
||||
if photo.path_edited:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited)}[/]"
|
||||
if photo.path_edited_live_photo:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path edited live video: ")
|
||||
path_str += f"[filepath]{format_path_link(photo.path_edited_live_photo)}[/]"
|
||||
if photo.path_raw:
|
||||
path_str += "\n"
|
||||
path_str += bold("Path raw: ")
|
||||
|
||||
@@ -8,7 +8,8 @@ import yaml
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION, _UNKNOWN_PLACE
|
||||
|
||||
from .common import DB_ARGUMENT, DB_OPTION, JSON_OPTION, get_photos_db
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, JSON_OPTION
|
||||
from .common import get_photos_db
|
||||
from .list import _list_libraries
|
||||
|
||||
|
||||
|
||||
@@ -11,22 +11,18 @@ from osxphotos.cli.click_rich_echo import (
|
||||
from osxphotos.debug import set_debug
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.phototemplate import RenderOptions
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
from osxphotos.queryoptions import query_options_from_kwargs
|
||||
|
||||
from .color_themes import get_default_theme
|
||||
from .common import (
|
||||
CLI_COLOR_ERROR,
|
||||
CLI_COLOR_WARNING,
|
||||
from .cli_params import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
FIELD_OPTION,
|
||||
JSON_OPTION,
|
||||
OSXPHOTOS_HIDDEN,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
)
|
||||
from .color_themes import get_default_theme
|
||||
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, OSXPHOTOS_HIDDEN, get_photos_db
|
||||
from .list import _list_libraries
|
||||
from .print_photo_info import print_photo_fields, print_photo_info
|
||||
from .verbose import get_verbose_console
|
||||
@@ -37,32 +33,6 @@ from .verbose import get_verbose_console
|
||||
@JSON_OPTION
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
|
||||
@click.option(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
)
|
||||
@click.option(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
)
|
||||
@click.option(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
)
|
||||
@click.option(
|
||||
"--add-to-album",
|
||||
metavar="ALBUM",
|
||||
@@ -89,9 +59,6 @@ from .verbose import get_verbose_console
|
||||
"Most useful with --quiet. "
|
||||
"May be repeated to print multiple template strings. ",
|
||||
)
|
||||
@click.option(
|
||||
"--debug", required=False, is_flag=True, default=False, hidden=OSXPHOTOS_HIDDEN
|
||||
)
|
||||
@DB_ARGUMENT
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
@@ -99,186 +66,33 @@ def query(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
photos_library,
|
||||
add_to_album,
|
||||
added_after,
|
||||
added_before,
|
||||
added_in_last,
|
||||
album,
|
||||
burst,
|
||||
cloudasset,
|
||||
deleted_only,
|
||||
deleted,
|
||||
description,
|
||||
duplicate,
|
||||
edited,
|
||||
exif,
|
||||
external_edit,
|
||||
favorite,
|
||||
field,
|
||||
folder,
|
||||
from_date,
|
||||
from_time,
|
||||
has_comment,
|
||||
has_likes,
|
||||
has_raw,
|
||||
hdr,
|
||||
hidden,
|
||||
ignore_case,
|
||||
in_album,
|
||||
incloud,
|
||||
is_reference,
|
||||
json_,
|
||||
keyword,
|
||||
label,
|
||||
live,
|
||||
location,
|
||||
max_size,
|
||||
min_size,
|
||||
missing,
|
||||
name,
|
||||
no_comment,
|
||||
no_description,
|
||||
no_likes,
|
||||
no_location,
|
||||
no_keyword,
|
||||
no_place,
|
||||
no_title,
|
||||
not_burst,
|
||||
not_cloudasset,
|
||||
not_favorite,
|
||||
not_hdr,
|
||||
not_hidden,
|
||||
not_in_album,
|
||||
not_incloud,
|
||||
not_live,
|
||||
not_missing,
|
||||
not_panorama,
|
||||
not_portrait,
|
||||
not_reference,
|
||||
not_screenshot,
|
||||
not_selfie,
|
||||
not_shared,
|
||||
not_slow_mo,
|
||||
not_time_lapse,
|
||||
only_movies,
|
||||
only_photos,
|
||||
panorama,
|
||||
person,
|
||||
place,
|
||||
portrait,
|
||||
print_template,
|
||||
query_eval,
|
||||
query_function,
|
||||
quiet,
|
||||
regex,
|
||||
screenshot,
|
||||
selected,
|
||||
selfie,
|
||||
shared,
|
||||
slow_mo,
|
||||
time_lapse,
|
||||
title,
|
||||
to_date,
|
||||
to_time,
|
||||
uti,
|
||||
uuid_from_file,
|
||||
uuid,
|
||||
year,
|
||||
debug, # handled in cli/__init__.py
|
||||
add_to_album,
|
||||
photos_library,
|
||||
**kwargs,
|
||||
):
|
||||
"""Query the Photos database using 1 or more search options;
|
||||
if more than one option is provided, they are treated as "AND"
|
||||
if more than one different option is provided, they are treated as "AND"
|
||||
(e.g. search for photos matching all options).
|
||||
"""
|
||||
If the same query option is provided multiple times, they are treated as
|
||||
"OR" (e.g. search for photos matching any of the options).
|
||||
|
||||
# if no query terms, show help and return
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
added_after,
|
||||
added_before,
|
||||
added_in_last,
|
||||
album,
|
||||
duplicate,
|
||||
edited,
|
||||
exif,
|
||||
external_edit,
|
||||
folder,
|
||||
from_date,
|
||||
from_time,
|
||||
has_raw,
|
||||
keyword,
|
||||
label,
|
||||
max_size,
|
||||
min_size,
|
||||
name,
|
||||
person,
|
||||
query_eval,
|
||||
query_function,
|
||||
regex,
|
||||
selected,
|
||||
to_date,
|
||||
to_time,
|
||||
uti,
|
||||
uuid_from_file,
|
||||
uuid,
|
||||
year,
|
||||
]
|
||||
exclusive = [
|
||||
(any(description), no_description),
|
||||
(any(place), no_place),
|
||||
(any(title), no_title),
|
||||
(any(keyword), no_keyword),
|
||||
(burst, not_burst),
|
||||
(cloudasset, not_cloudasset),
|
||||
(deleted, deleted_only),
|
||||
(favorite, not_favorite),
|
||||
(has_comment, no_comment),
|
||||
(has_likes, no_likes),
|
||||
(hdr, not_hdr),
|
||||
(hidden, not_hidden),
|
||||
(in_album, not_in_album),
|
||||
(incloud, not_incloud),
|
||||
(live, not_live),
|
||||
(location, no_location),
|
||||
(missing, not_missing),
|
||||
(only_photos, only_movies),
|
||||
(panorama, not_panorama),
|
||||
(portrait, not_portrait),
|
||||
(screenshot, not_screenshot),
|
||||
(selfie, not_selfie),
|
||||
(shared, not_shared),
|
||||
(slow_mo, not_slow_mo),
|
||||
(time_lapse, not_time_lapse),
|
||||
(is_reference, not_reference),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
if any(all(bb) for bb in exclusive) or not any(
|
||||
nonexclusive + [b ^ n for b, n in exclusive]
|
||||
):
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(ctx.obj.group.commands["query"].get_help(ctx), err=True)
|
||||
return
|
||||
For example:
|
||||
|
||||
osxphotos query --person "John Doe" --person "Jane Doe" --keyword "vacation"
|
||||
|
||||
will return all photos with either person of ("John Doe" OR "Jane Doe") AND keyword of "vacation"
|
||||
|
||||
If not query options are provided, all photos in the library will be returned.
|
||||
"""
|
||||
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(get_default_theme())
|
||||
|
||||
# actually have something to query
|
||||
# default searches for everything
|
||||
photos = True
|
||||
movies = True
|
||||
if only_movies:
|
||||
photos = False
|
||||
if only_photos:
|
||||
movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
if uuid_from_file:
|
||||
uuid_list = list(uuid) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(uuid_from_file))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_db = cli_obj.db if cli_obj is not None else None
|
||||
db = get_photos_db(*photos_library, db, cli_db)
|
||||
@@ -288,99 +102,23 @@ def query(
|
||||
_list_libraries()
|
||||
return
|
||||
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except Exception as e:
|
||||
raise click.BadOptionUsage("query", str(e)) from e
|
||||
|
||||
photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
query_options = QueryOptions(
|
||||
added_after=added_after,
|
||||
added_before=added_before,
|
||||
added_in_last=added_in_last,
|
||||
album=album,
|
||||
burst=burst,
|
||||
cloudasset=cloudasset,
|
||||
deleted_only=deleted_only,
|
||||
deleted=deleted,
|
||||
description=description,
|
||||
duplicate=duplicate,
|
||||
edited=edited,
|
||||
exif=exif,
|
||||
external_edit=external_edit,
|
||||
favorite=favorite,
|
||||
folder=folder,
|
||||
from_date=from_date,
|
||||
from_time=from_time,
|
||||
function=query_function,
|
||||
has_comment=has_comment,
|
||||
has_likes=has_likes,
|
||||
has_raw=has_raw,
|
||||
hdr=hdr,
|
||||
hidden=hidden,
|
||||
ignore_case=ignore_case,
|
||||
in_album=in_album,
|
||||
incloud=incloud,
|
||||
is_reference=is_reference,
|
||||
keyword=keyword,
|
||||
label=label,
|
||||
live=live,
|
||||
location=location,
|
||||
max_size=max_size,
|
||||
min_size=min_size,
|
||||
missing=missing,
|
||||
movies=movies,
|
||||
name=name,
|
||||
no_comment=no_comment,
|
||||
no_description=no_description,
|
||||
no_likes=no_likes,
|
||||
no_location=no_location,
|
||||
no_keyword=no_keyword,
|
||||
no_place=no_place,
|
||||
no_title=no_title,
|
||||
not_burst=not_burst,
|
||||
not_cloudasset=not_cloudasset,
|
||||
not_favorite=not_favorite,
|
||||
not_hdr=not_hdr,
|
||||
not_hidden=not_hidden,
|
||||
not_in_album=not_in_album,
|
||||
not_incloud=not_incloud,
|
||||
not_live=not_live,
|
||||
not_missing=not_missing,
|
||||
not_panorama=not_panorama,
|
||||
not_portrait=not_portrait,
|
||||
not_reference=not_reference,
|
||||
not_screenshot=not_screenshot,
|
||||
not_selfie=not_selfie,
|
||||
not_shared=not_shared,
|
||||
not_slow_mo=not_slow_mo,
|
||||
not_time_lapse=not_time_lapse,
|
||||
panorama=panorama,
|
||||
person=person,
|
||||
photos=photos,
|
||||
place=place,
|
||||
portrait=portrait,
|
||||
query_eval=query_eval,
|
||||
regex=regex,
|
||||
screenshot=screenshot,
|
||||
selected=selected,
|
||||
selfie=selfie,
|
||||
shared=shared,
|
||||
slow_mo=slow_mo,
|
||||
time_lapse=time_lapse,
|
||||
title=title,
|
||||
to_date=to_date,
|
||||
to_time=to_time,
|
||||
uti=uti,
|
||||
uuid=uuid,
|
||||
year=year,
|
||||
)
|
||||
|
||||
try:
|
||||
photos = photosdb.query(query_options)
|
||||
except ValueError as e:
|
||||
if "Invalid query_eval CRITERIA:" in str(e):
|
||||
msg = str(e).split(":")[1]
|
||||
raise click.BadOptionUsage(
|
||||
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
|
||||
)
|
||||
else:
|
||||
raise ValueError(e)
|
||||
if "Invalid query_eval CRITERIA:" not in str(e):
|
||||
raise ValueError(e) from e
|
||||
|
||||
msg = str(e).split(":")[1]
|
||||
raise click.BadOptionUsage(
|
||||
"query_eval", f"Invalid query-eval CRITERIA: {msg}"
|
||||
) from e
|
||||
|
||||
# below needed for to make CliRunner work for testing
|
||||
cli_json = cli_obj.json if cli_obj is not None else None
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"""repl command for osxphotos CLI"""
|
||||
|
||||
import dataclasses
|
||||
import os
|
||||
import os.path
|
||||
import pathlib
|
||||
@@ -14,23 +13,18 @@ from rich import pretty, print
|
||||
|
||||
import osxphotos
|
||||
from osxphotos._constants import _PHOTOS_4_VERSION
|
||||
from osxphotos.cli.click_rich_echo import rich_echo_error as echo_error
|
||||
from osxphotos.photoinfo import PhotoInfo
|
||||
from osxphotos.photosdb import PhotosDB
|
||||
from osxphotos.pyrepl import embed_repl
|
||||
from osxphotos.queryoptions import QueryOptions
|
||||
|
||||
from .common import (
|
||||
DB_ARGUMENT,
|
||||
DB_OPTION,
|
||||
DELETED_OPTIONS,
|
||||
QUERY_OPTIONS,
|
||||
get_photos_db,
|
||||
load_uuid_from_file,
|
||||
from osxphotos.queryoptions import (
|
||||
IncompatibleQueryOptions,
|
||||
QueryOptions,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
|
||||
|
||||
class IncompatibleQueryOptions(Exception):
|
||||
pass
|
||||
from .cli_params import DB_ARGUMENT, DB_OPTION, DELETED_OPTIONS, QUERY_OPTIONS
|
||||
from .common import get_photos_db
|
||||
|
||||
|
||||
@click.command(name="repl")
|
||||
@@ -53,32 +47,6 @@ class IncompatibleQueryOptions(Exception):
|
||||
)
|
||||
@QUERY_OPTIONS
|
||||
@DELETED_OPTIONS
|
||||
@click.option("--missing", is_flag=True, help="Search for photos missing from disk.")
|
||||
@click.option(
|
||||
"--not-missing",
|
||||
is_flag=True,
|
||||
help="Search for photos present on disk (e.g. not missing).",
|
||||
)
|
||||
@click.option(
|
||||
"--cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--not-cloudasset",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not part of an iCloud library",
|
||||
)
|
||||
@click.option(
|
||||
"--incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are in iCloud (have been synched)",
|
||||
)
|
||||
@click.option(
|
||||
"--not-incloud",
|
||||
is_flag=True,
|
||||
help="Search for photos that are not in iCloud (have not been synched)",
|
||||
)
|
||||
def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
"""Run interactive osxphotos REPL shell (useful for debugging, prototyping, and inspecting your Photos library)"""
|
||||
import logging
|
||||
@@ -100,6 +68,11 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
logger.disabled = True
|
||||
|
||||
pretty.install()
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions as e:
|
||||
echo_error(f"Incompatible query options: {e}")
|
||||
ctx.exit(1)
|
||||
print(f"python version: {sys.version}")
|
||||
print(f"osxphotos version: {osxphotos._version.__version__}")
|
||||
db = db or get_photos_db()
|
||||
@@ -110,12 +83,6 @@ def repl(ctx, cli_obj, db, emacs, beta, **kwargs):
|
||||
print("Beta mode enabled")
|
||||
print("Getting photos")
|
||||
tic = time.perf_counter()
|
||||
try:
|
||||
query_options = _query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions:
|
||||
click.echo("Incompatible query options", err=True)
|
||||
click.echo(ctx.obj.group.commands["repl"].get_help(ctx), err=True)
|
||||
sys.exit(1)
|
||||
photos = _query_photos(photosdb, query_options)
|
||||
all_photos = _get_all_photos(photosdb)
|
||||
toc = time.perf_counter()
|
||||
@@ -237,99 +204,6 @@ def _spotlight_photo(photo: PhotoInfo):
|
||||
photo_.spotlight()
|
||||
|
||||
|
||||
def _query_options_from_kwargs(**kwargs) -> QueryOptions:
|
||||
"""Validate query options and create a QueryOptions instance"""
|
||||
# sanity check input args
|
||||
nonexclusive = [
|
||||
"added_after",
|
||||
"added_before",
|
||||
"added_in_last",
|
||||
"album",
|
||||
"duplicate",
|
||||
"edited",
|
||||
"exif",
|
||||
"external_edit",
|
||||
"folder",
|
||||
"from_date",
|
||||
"from_time",
|
||||
"has_raw",
|
||||
"keyword",
|
||||
"label",
|
||||
"max_size",
|
||||
"min_size",
|
||||
"name",
|
||||
"person",
|
||||
"query_eval",
|
||||
"query_function",
|
||||
"regex",
|
||||
"selected",
|
||||
"to_date",
|
||||
"to_time",
|
||||
"uti",
|
||||
"uuid_from_file",
|
||||
"uuid",
|
||||
"year",
|
||||
]
|
||||
exclusive = [
|
||||
("burst", "not_burst"),
|
||||
("cloudasset", "not_cloudasset"),
|
||||
("deleted", "deleted_only"),
|
||||
("favorite", "not_favorite"),
|
||||
("has_comment", "no_comment"),
|
||||
("has_likes", "no_likes"),
|
||||
("hdr", "not_hdr"),
|
||||
("hidden", "not_hidden"),
|
||||
("in_album", "not_in_album"),
|
||||
("incloud", "not_incloud"),
|
||||
("live", "not_live"),
|
||||
("location", "no_location"),
|
||||
("keyword", "no_keyword"),
|
||||
("missing", "not_missing"),
|
||||
("only_photos", "only_movies"),
|
||||
("panorama", "not_panorama"),
|
||||
("portrait", "not_portrait"),
|
||||
("screenshot", "not_screenshot"),
|
||||
("selfie", "not_selfie"),
|
||||
("shared", "not_shared"),
|
||||
("slow_mo", "not_slow_mo"),
|
||||
("time_lapse", "not_time_lapse"),
|
||||
("is_reference", "not_reference"),
|
||||
]
|
||||
# print help if no non-exclusive term or a double exclusive term is given
|
||||
# TODO: add option to validate requiring at least one query arg
|
||||
if any(all([kwargs[b], kwargs[n]]) for b, n in exclusive) or any(
|
||||
[
|
||||
all([any(kwargs["title"]), kwargs["no_title"]]),
|
||||
all([any(kwargs["description"]), kwargs["no_description"]]),
|
||||
all([any(kwargs["place"]), kwargs["no_place"]]),
|
||||
all([any(kwargs["keyword"]), kwargs["no_keyword"]]),
|
||||
]
|
||||
):
|
||||
raise IncompatibleQueryOptions
|
||||
|
||||
# actually have something to query
|
||||
include_photos = True
|
||||
include_movies = True # default searches for everything
|
||||
if kwargs["only_movies"]:
|
||||
include_photos = False
|
||||
if kwargs["only_photos"]:
|
||||
include_movies = False
|
||||
|
||||
# load UUIDs if necessary and append to any uuids passed with --uuid
|
||||
uuid = None
|
||||
if kwargs["uuid_from_file"]:
|
||||
uuid_list = list(kwargs["uuid"]) # Click option is a tuple
|
||||
uuid_list.extend(load_uuid_from_file(kwargs["uuid_from_file"]))
|
||||
uuid = tuple(uuid_list)
|
||||
|
||||
query_fields = [field.name for field in dataclasses.fields(QueryOptions)]
|
||||
query_dict = {field: kwargs.get(field) for field in query_fields}
|
||||
query_dict["photos"] = include_photos
|
||||
query_dict["movies"] = include_movies
|
||||
query_dict["uuid"] = uuid
|
||||
return QueryOptions(**query_dict)
|
||||
|
||||
|
||||
def _query_photos(photosdb: PhotosDB, query_options: QueryOptions) -> List:
|
||||
"""Query photos given a QueryOptions instance"""
|
||||
try:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Report writer for the --report option of `osxphotos export`"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import csv
|
||||
import datetime
|
||||
@@ -15,20 +16,28 @@ from osxphotos.export_db import OSXPHOTOS_ABOUT_STRING
|
||||
from osxphotos.photoexporter import ExportResults
|
||||
from osxphotos.sqlite_utils import sqlite_columns
|
||||
|
||||
from .sync_results import SyncResults
|
||||
|
||||
__all__ = [
|
||||
"report_writer_factory",
|
||||
"ExportReportWriterCSV",
|
||||
"ExportReportWriterJSON",
|
||||
"ExportReportWriterSqlite",
|
||||
"ReportWriterABC",
|
||||
"ReportWriterCSV",
|
||||
"ReportWriterSqlite",
|
||||
"ReportWriterNoOp",
|
||||
"SyncReportWriterCSV",
|
||||
"SyncReportWriterJSON",
|
||||
"SyncReportWriterSqlite",
|
||||
"export_report_writer_factory",
|
||||
"sync_report_writer_factory",
|
||||
]
|
||||
|
||||
|
||||
# Abstract base class for report writers
|
||||
class ReportWriterABC(ABC):
|
||||
"""Abstract base class for report writers"""
|
||||
|
||||
@abstractmethod
|
||||
def write(self, export_results: ExportResults):
|
||||
def write(self, results: ExportResults | SyncResults):
|
||||
"""Write results to the output file"""
|
||||
pass
|
||||
|
||||
@@ -38,13 +47,16 @@ class ReportWriterABC(ABC):
|
||||
pass
|
||||
|
||||
|
||||
# Report writer that does nothing, used for --dry-run or when --report not specified
|
||||
|
||||
|
||||
class ReportWriterNoOp(ABC):
|
||||
"""Report writer that does nothing"""
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
def write(self, results: ExportResults | SyncResults):
|
||||
"""Write results to the output file"""
|
||||
pass
|
||||
|
||||
@@ -53,8 +65,9 @@ class ReportWriterNoOp(ABC):
|
||||
pass
|
||||
|
||||
|
||||
class ReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file"""
|
||||
# Classes for writing ExportResults to report file
|
||||
class ExportReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file for export results"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -95,7 +108,7 @@ class ReportWriterCSV(ReportWriterABC):
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
all_results = prepare_results_for_writing(export_results)
|
||||
all_results = prepare_export_results_for_writing(export_results)
|
||||
for data in list(all_results.values()):
|
||||
self._csv_writer.writerow(data)
|
||||
self._output_fh.flush()
|
||||
@@ -109,8 +122,8 @@ class ReportWriterCSV(ReportWriterABC):
|
||||
self._output_fh.close()
|
||||
|
||||
|
||||
class ReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON report file"""
|
||||
class ExportReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON report file for export results"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -134,7 +147,9 @@ class ReportWriterJSON(ReportWriterABC):
|
||||
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
all_results = prepare_results_for_writing(export_results, bool_values=True)
|
||||
all_results = prepare_export_results_for_writing(
|
||||
export_results, bool_values=True
|
||||
)
|
||||
for data in list(all_results.values()):
|
||||
if self._first_record_written:
|
||||
self._output_fh.write(",\n")
|
||||
@@ -153,8 +168,8 @@ class ReportWriterJSON(ReportWriterABC):
|
||||
self.close()
|
||||
|
||||
|
||||
class ReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite report file"""
|
||||
class ExportReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite report file for export data"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
@@ -173,7 +188,7 @@ class ReportWriterSQLite(ReportWriterABC):
|
||||
def write(self, export_results: ExportResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
all_results = prepare_results_for_writing(export_results)
|
||||
all_results = prepare_export_results_for_writing(export_results)
|
||||
for data in list(all_results.values()):
|
||||
data["report_id"] = self.report_id
|
||||
cursor = self._conn.cursor()
|
||||
@@ -284,7 +299,7 @@ class ReportWriterSQLite(ReportWriterABC):
|
||||
self.close()
|
||||
|
||||
|
||||
def prepare_results_for_writing(
|
||||
def prepare_export_results_for_writing(
|
||||
export_results: ExportResults, bool_values: bool = False
|
||||
) -> Dict:
|
||||
"""Return all results for writing to report
|
||||
@@ -406,17 +421,250 @@ def prepare_results_for_writing(
|
||||
return all_results
|
||||
|
||||
|
||||
def report_writer_factory(
|
||||
def export_report_writer_factory(
|
||||
output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
) -> ReportWriterABC:
|
||||
"""Return a ReportWriter instance appropriate for the output file type"""
|
||||
output_type = os.path.splitext(output_file)[1]
|
||||
output_type = output_type.lower()[1:]
|
||||
if output_type == "csv":
|
||||
return ReportWriterCSV(output_file, append)
|
||||
return ExportReportWriterCSV(output_file, append)
|
||||
elif output_type == "json":
|
||||
return ReportWriterJSON(output_file, append)
|
||||
return ExportReportWriterJSON(output_file, append)
|
||||
elif output_type in ["sqlite", "db"]:
|
||||
return ReportWriterSQLite(output_file, append)
|
||||
return ExportReportWriterSQLite(output_file, append)
|
||||
else:
|
||||
raise ValueError(f"Unknown report file type: {output_file}")
|
||||
|
||||
|
||||
# Classes for writing Sync results to a report file
|
||||
|
||||
|
||||
class SyncReportWriterCSV(ReportWriterABC):
|
||||
"""Write CSV report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
mode = "a" if append else "w"
|
||||
self._output_fh = open(self.output_file, mode)
|
||||
|
||||
def write(self, sync_results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
report_columns = sync_results.results_header
|
||||
self._csv_writer = csv.DictWriter(self._output_fh, fieldnames=report_columns)
|
||||
if not self.append:
|
||||
self._csv_writer.writeheader()
|
||||
|
||||
for data in sync_results.results_list:
|
||||
self._csv_writer.writerow(dict(zip(report_columns, data)))
|
||||
self._output_fh.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._output_fh.close()
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self._output_fh.close()
|
||||
|
||||
|
||||
class SyncReportWriterJSON(ReportWriterABC):
|
||||
"""Write JSON SyncResults report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
self.indent = 4
|
||||
|
||||
self._first_record_written = False
|
||||
if append:
|
||||
with open(self.output_file, "r") as fh:
|
||||
existing_data = json.load(fh)
|
||||
self._output_fh = open(self.output_file, "w")
|
||||
self._output_fh.write("[")
|
||||
for data in existing_data:
|
||||
self._output_fh.write(json.dumps(data, indent=self.indent))
|
||||
self._output_fh.write(",\n")
|
||||
else:
|
||||
self._output_fh = open(self.output_file, "w")
|
||||
self._output_fh.write("[")
|
||||
|
||||
def write(self, results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
# convert datetimes to strings
|
||||
def default(o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
for data in list(results.results_dict.values()):
|
||||
if self._first_record_written:
|
||||
self._output_fh.write(",\n")
|
||||
else:
|
||||
self._first_record_written = True
|
||||
self._output_fh.write(json.dumps(data, indent=self.indent, default=default))
|
||||
self._output_fh.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._output_fh.write("]")
|
||||
self._output_fh.close()
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self.close()
|
||||
|
||||
|
||||
class SyncReportWriterSQLite(ReportWriterABC):
|
||||
"""Write sqlite SyncResults report file"""
|
||||
|
||||
def __init__(
|
||||
self, output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
):
|
||||
self.output_file = output_file
|
||||
self.append = append
|
||||
|
||||
if not append:
|
||||
with suppress(FileNotFoundError):
|
||||
os.unlink(self.output_file)
|
||||
|
||||
self._conn = sqlite3.connect(self.output_file)
|
||||
self._create_tables()
|
||||
self.report_id = self._generate_report_id()
|
||||
|
||||
def write(self, results: SyncResults):
|
||||
"""Write results to the output file"""
|
||||
|
||||
# insert rows of values into sqlite report table
|
||||
for row in list(results.results_list):
|
||||
report_id = self.report_id
|
||||
data = [str(v) if v else "" for v in row]
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute(
|
||||
"INSERT INTO report "
|
||||
"(report_id, uuid, filename, fingerprint, updated, "
|
||||
"albums_updated, albums_datetime, albums_before, albums_after, "
|
||||
"description_updated, description_datetime, description_before, description_after, "
|
||||
"favorite_updated, favorite_datetime, favorite_before, favorite_after, "
|
||||
"keywords_updated, keywords_datetime, keywords_before, keywords_after, "
|
||||
"title_updated, title_datetime, title_before, title_after)"
|
||||
"VALUES "
|
||||
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
(report_id, *data),
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def close(self):
|
||||
"""Close the output file"""
|
||||
self._conn.close()
|
||||
|
||||
def _create_tables(self):
|
||||
c = self._conn.cursor()
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS report (
|
||||
report_id TEXT,
|
||||
uuid TEXT,
|
||||
filename TEXT,
|
||||
fingerprint TEXT,
|
||||
updated INT,
|
||||
albums_updated INT,
|
||||
albums_datetime TEXT,
|
||||
albums_before TEXT,
|
||||
albums_after TEXT,
|
||||
description_updated INT,
|
||||
description_datetime TEXT,
|
||||
description_before TEXT,
|
||||
description_after TEXT,
|
||||
favorite_updated INT,
|
||||
favorite_datetime TEXT,
|
||||
favorite_before TEXT,
|
||||
favorite_after TEXT,
|
||||
keywords_updated INT,
|
||||
keywords_datetime TEXT,
|
||||
keywords_before TEXT,
|
||||
keywords_after TEXT,
|
||||
title_updated INT,
|
||||
title_datetime TEXT,
|
||||
title_before TEXT,
|
||||
title_after TEXT
|
||||
);
|
||||
"""
|
||||
)
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS about (
|
||||
id INTEGER PRIMARY KEY,
|
||||
about TEXT
|
||||
);"""
|
||||
)
|
||||
c.execute(
|
||||
"INSERT INTO about(about) VALUES (?);",
|
||||
(f"OSXPhotos Sync Report. {OSXPHOTOS_ABOUT_STRING}",),
|
||||
)
|
||||
c.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS report_id (
|
||||
report_id INTEGER PRIMARY KEY,
|
||||
datetime TEXT
|
||||
);"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
# create report_summary view
|
||||
c.execute("DROP VIEW IF EXISTS report_summary;")
|
||||
c.execute(
|
||||
"""
|
||||
CREATE VIEW report_summary AS
|
||||
SELECT
|
||||
r.report_id,
|
||||
i.datetime AS report_datetime,
|
||||
COUNT(r.uuid) as processed,
|
||||
COUNT(CASE r.updated WHEN 'True' THEN 1 ELSE NULL END) as updated,
|
||||
COUNT(case r.albums_updated WHEN 'True' THEN 1 ELSE NULL END) as albums_updated,
|
||||
COUNT(case r.description_updated WHEN 'True' THEN 1 ELSE NULL END) as description_updated,
|
||||
COUNT(case r.favorite_updated WHEN 'True' THEN 1 ELSE NULL END) as favorite_updated,
|
||||
COUNT(case r.keywords_updated WHEN 'True' THEN 1 ELSE NULL END) as keywords_updated,
|
||||
COUNT(case r.title_updated WHEN 'True' THEN 1 ELSE NULL END) as title_updated
|
||||
FROM report as r
|
||||
INNER JOIN report_id as i ON r.report_id = i.report_id
|
||||
GROUP BY r.report_id;
|
||||
"""
|
||||
)
|
||||
self._conn.commit()
|
||||
|
||||
def _generate_report_id(self) -> int:
|
||||
"""Get a new report ID for this report"""
|
||||
c = self._conn.cursor()
|
||||
c.execute(
|
||||
"INSERT INTO report_id(datetime) VALUES (?);",
|
||||
(datetime.datetime.now().isoformat(),),
|
||||
)
|
||||
report_id = c.lastrowid
|
||||
self._conn.commit()
|
||||
return report_id
|
||||
|
||||
def __del__(self):
|
||||
with suppress(Exception):
|
||||
self.close()
|
||||
|
||||
|
||||
def sync_report_writer_factory(
|
||||
output_file: Union[str, bytes, os.PathLike], append: bool = False
|
||||
) -> ReportWriterABC:
|
||||
"""Return a ReportWriter instance appropriate for the output file type"""
|
||||
output_type = os.path.splitext(output_file)[1]
|
||||
output_type = output_type.lower()[1:]
|
||||
if output_type == "csv":
|
||||
return SyncReportWriterCSV(output_file, append)
|
||||
elif output_type == "json":
|
||||
return SyncReportWriterJSON(output_file, append)
|
||||
elif output_type in ["sqlite", "db"]:
|
||||
return SyncReportWriterSQLite(output_file, append)
|
||||
else:
|
||||
raise ValueError(f"Unknown report file type: {output_file}")
|
||||
|
||||
95
osxphotos/cli/show_command.py
Normal file
95
osxphotos/cli/show_command.py
Normal file
@@ -0,0 +1,95 @@
|
||||
"""osxphotos show command"""
|
||||
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
import click
|
||||
|
||||
from osxphotos._constants import UUID_PATTERN
|
||||
from osxphotos.export_db_utils import get_uuid_for_filepath
|
||||
from osxphotos.photoscript_utils import (
|
||||
photoscript_object_from_name,
|
||||
photoscript_object_from_uuid,
|
||||
)
|
||||
from osxphotos.photosdb.photosdb_utils import get_photos_library_version
|
||||
from osxphotos.utils import get_last_library_path
|
||||
|
||||
from .cli_commands import echo, echo_error
|
||||
from .cli_params import DB_OPTION
|
||||
from .click_rich_echo import set_rich_theme
|
||||
|
||||
|
||||
@click.command(name="show")
|
||||
@DB_OPTION
|
||||
@click.argument("uuid_or_name", metavar="UUID_OR_NAME", nargs=1, required=True)
|
||||
@click.pass_context
|
||||
def show(ctx, db, uuid_or_name):
|
||||
"""Show photo, album, or folder in Photos from UUID_OR_NAME
|
||||
|
||||
Examples:
|
||||
|
||||
osxphotos show 12345678-1234-1234-1234-123456789012
|
||||
|
||||
osxphotos show "My Album"
|
||||
|
||||
osxphotos show "My Folder"
|
||||
|
||||
osxphotos show IMG_1234.JPG
|
||||
|
||||
show can also be used to show a photo exported with `osxphotos export`:
|
||||
|
||||
osxphotos show /path/to/exported/photo.jpg
|
||||
|
||||
In this case, the UUID_OR_NAME is the path to the exported photo and osxphotos
|
||||
will attempt to find the export database to match the photo to the original in
|
||||
Photos. If your export database is not in the default location in the root of the
|
||||
export directory, this will not work.
|
||||
|
||||
Notes:
|
||||
|
||||
This command requires Photos library version 5 or higher.
|
||||
Currently this command cannot be used to show subfolders in Photos.
|
||||
"""
|
||||
db = db or get_last_library_path()
|
||||
if not db:
|
||||
echo(
|
||||
"Could not find Photos library. Use --library/--db to specify path to Photos library."
|
||||
)
|
||||
ctx.exit(1)
|
||||
|
||||
if get_photos_library_version(db) < 5:
|
||||
echo_error("[error]show command requires Photos library version 5 or higher")
|
||||
ctx.exit(1)
|
||||
|
||||
try:
|
||||
if re.match(UUID_PATTERN, uuid_or_name):
|
||||
if not (obj := photoscript_object_from_uuid(uuid_or_name, db)):
|
||||
raise ValueError(
|
||||
f"could not find asset with UUID [uuid]{uuid_or_name}[/]"
|
||||
)
|
||||
obj_type = obj.__class__.__name__
|
||||
echo(f"Found [filename]{obj_type}[/] with UUID: [uuid]{uuid_or_name}[/]")
|
||||
obj.spotlight()
|
||||
elif obj := photoscript_object_from_name(uuid_or_name, db):
|
||||
obj_type = obj.__class__.__name__
|
||||
echo(
|
||||
f"Found [filename]{obj_type}[/] with name: [filepath]{uuid_or_name}[/]"
|
||||
)
|
||||
obj.spotlight()
|
||||
elif uuid := get_uuid_for_filepath(pathlib.Path(uuid_or_name).resolve()):
|
||||
if not (obj := photoscript_object_from_uuid(uuid, db)):
|
||||
raise ValueError(
|
||||
f"could not find asset with UUID [uuid]{uuid}[/] for file [filepath]{uuid_or_name}[/]"
|
||||
)
|
||||
obj_type = obj.__class__.__name__
|
||||
echo(
|
||||
f"Found [filename]{obj_type}[/] from export database: [filepath]{uuid_or_name}[/]"
|
||||
)
|
||||
obj.spotlight()
|
||||
else:
|
||||
raise ValueError(
|
||||
f"could not find asset with name [filepath]{uuid_or_name}[/]"
|
||||
)
|
||||
except Exception as e:
|
||||
echo_error(f"[error]Error finding asset [uuid]{uuid_or_name}[/]: {e}")
|
||||
ctx.exit(1)
|
||||
@@ -12,7 +12,8 @@ from rich.syntax import Syntax
|
||||
|
||||
import osxphotos
|
||||
|
||||
from .common import DB_OPTION, OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
|
||||
from .cli_params import DB_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .common import OSXPHOTOS_SNAPSHOT_DIR, get_photos_db
|
||||
from .verbose import verbose_print
|
||||
|
||||
|
||||
@@ -30,7 +31,7 @@ def snap(ctx, cli_obj, db):
|
||||
Works only on Photos library versions since Catalina (10.15) or newer.
|
||||
"""
|
||||
|
||||
db = get_photos_db(db, cli_obj.db)
|
||||
db = get_photos_db(db, cli_obj.db if cli_obj else None)
|
||||
db_path = pathlib.Path(db)
|
||||
if db_path.is_file():
|
||||
# assume it's the sqlite file
|
||||
@@ -80,8 +81,9 @@ def snap(ctx, cli_obj, db):
|
||||
"Default is 'monokai'.",
|
||||
)
|
||||
@click.argument("db2", nargs=-1, type=click.Path(exists=True))
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Print verbose output.")
|
||||
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
def diff(ctx, cli_obj, db, raw_output, style, db2, verbose_flag, timestamp):
|
||||
"""Compare two Photos databases and print out differences
|
||||
|
||||
To use the diff command, you'll need to install sqldiff via homebrew:
|
||||
@@ -110,7 +112,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
Works only on Photos library versions since Catalina (10.15) or newer.
|
||||
"""
|
||||
|
||||
verbose_ = verbose_print(verbose, rich=True)
|
||||
verbose = verbose_print(verbose_flag, timestamp=timestamp)
|
||||
|
||||
sqldiff = shutil.which("sqldiff")
|
||||
if not sqldiff:
|
||||
@@ -118,9 +120,9 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
"sqldiff not found; install via homebrew (https://brew.sh/): `brew install sqldiff`"
|
||||
)
|
||||
ctx.exit(2)
|
||||
verbose_(f"sqldiff found at '{sqldiff}'")
|
||||
verbose(f"sqldiff found at '{sqldiff}'")
|
||||
|
||||
db = get_photos_db(db, cli_obj.db)
|
||||
db = get_photos_db(db, cli_obj.db if cli_obj else None)
|
||||
db_path = pathlib.Path(db)
|
||||
if db_path.is_file():
|
||||
# assume it's the sqlite file
|
||||
@@ -133,7 +135,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
else:
|
||||
# get most recent snapshot
|
||||
db_folder = os.environ.get("OSXPHOTOS_SNAPSHOT", OSXPHOTOS_SNAPSHOT_DIR)
|
||||
verbose_(f"Using snapshot folder: '{db_folder}'")
|
||||
verbose(f"Using snapshot folder: '{db_folder}'")
|
||||
folders = sorted([f for f in pathlib.Path(db_folder).glob("*") if f.is_dir()])
|
||||
folder_2 = folders[-1]
|
||||
db_2 = folder_2 / "Photos.sqlite"
|
||||
@@ -143,7 +145,7 @@ def diff(ctx, cli_obj, db, raw_output, style, db2, verbose):
|
||||
if not db_2.exists():
|
||||
print(f"database file {db_2} missing")
|
||||
|
||||
verbose_(f"Comparing databases {db_1} and {db_2}")
|
||||
verbose(f"Comparing databases {db_1} and {db_2}")
|
||||
|
||||
diff_proc = subprocess.Popen([sqldiff, db_2, db_1], stdout=subprocess.PIPE)
|
||||
console = Console()
|
||||
|
||||
769
osxphotos/cli/sync.py
Normal file
769
osxphotos/cli/sync.py
Normal file
@@ -0,0 +1,769 @@
|
||||
"""Sync metadata and albums between Photos libraries"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
from typing import Any, Callable, Literal
|
||||
|
||||
import click
|
||||
import photoscript
|
||||
|
||||
from osxphotos import PhotoInfo, PhotosDB, __version__
|
||||
from osxphotos.photoinfo import PhotoInfoNone
|
||||
from osxphotos.photosalbum import PhotosAlbum
|
||||
from osxphotos.photosdb.photosdb_utils import get_db_version
|
||||
from osxphotos.phototemplate import PhotoTemplate, RenderOptions
|
||||
from osxphotos.queryoptions import (
|
||||
IncompatibleQueryOptions,
|
||||
QueryOptions,
|
||||
query_options_from_kwargs,
|
||||
)
|
||||
from osxphotos.sqlitekvstore import SQLiteKVStore
|
||||
from osxphotos.utils import pluralize
|
||||
|
||||
from .cli_params import (
|
||||
DB_OPTION,
|
||||
QUERY_OPTIONS,
|
||||
THEME_OPTION,
|
||||
TIMESTAMP_OPTION,
|
||||
VERBOSE_OPTION,
|
||||
)
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .param_types import TemplateString
|
||||
from .report_writer import sync_report_writer_factory
|
||||
from .rich_progress import rich_progress
|
||||
from .sync_results import SYNC_PROPERTIES, SyncResults
|
||||
from .verbose import get_verbose_console, verbose_print
|
||||
|
||||
SYNC_ABOUT_STRING = (
|
||||
f"Sync Metadata Database created by osxphotos version {__version__} "
|
||||
+ f"(https://github.com/RhetTbull/osxphotos) on {datetime.datetime.now()}"
|
||||
)
|
||||
|
||||
SYNC_IMPORT_TYPES = [
|
||||
"keywords",
|
||||
"albums",
|
||||
"title",
|
||||
"description",
|
||||
"favorite",
|
||||
]
|
||||
SYNC_IMPORT_TYPES_ALL = ["all"] + SYNC_IMPORT_TYPES
|
||||
|
||||
|
||||
class SyncImportPath(click.ParamType):
|
||||
"""A path to a Photos library or a metadata export file created by --export"""
|
||||
|
||||
name = "SYNC_IMPORT_PATH"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
if not pathlib.Path(value).exists():
|
||||
self.fail(f"{value} is not a file or directory")
|
||||
value = str(pathlib.Path(value).expanduser().resolve())
|
||||
# call get_import_type to raise exception if not a valid import type
|
||||
get_import_type(value)
|
||||
return value
|
||||
except Exception as e:
|
||||
self.fail(f"Could not determine import type for {value}: {e}")
|
||||
|
||||
|
||||
class SyncImportType(click.ParamType):
|
||||
"""A string indicating which metadata to set or merge from the import source"""
|
||||
|
||||
# valid values are specified in METADATA_IMPORT_TYPES_ALL
|
||||
|
||||
name = "SYNC_IMPORT_TYPE"
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
if value not in SYNC_IMPORT_TYPES_ALL:
|
||||
values = [v.strip() for v in value.split(",")]
|
||||
for v in values:
|
||||
if v not in SYNC_IMPORT_TYPES_ALL:
|
||||
self.fail(
|
||||
f"{v} is not a valid import type, valid values are {', '.join(SYNC_IMPORT_TYPES_ALL)}"
|
||||
)
|
||||
return value
|
||||
except Exception as e:
|
||||
self.fail(f"Could not determine import type for {value}: {e}")
|
||||
|
||||
|
||||
def render_and_validate_report(report: str) -> str:
|
||||
"""Render a report file template and validate the filename
|
||||
|
||||
Args:
|
||||
report: the template string
|
||||
|
||||
Returns:
|
||||
the rendered report filename
|
||||
|
||||
Note:
|
||||
Exits with error if the report filename is invalid
|
||||
"""
|
||||
# render report template and validate the filename
|
||||
template = PhotoTemplate(PhotoInfoNone())
|
||||
render_options = RenderOptions()
|
||||
report_file, _ = template.render(report, options=render_options)
|
||||
report = report_file[0]
|
||||
|
||||
if os.path.isdir(report):
|
||||
echo(
|
||||
f"[error]Report '{report}' is a directory, must be file name",
|
||||
err=True,
|
||||
)
|
||||
raise click.Abort()
|
||||
return report
|
||||
|
||||
|
||||
def parse_set_merge(values: tuple[str]) -> tuple[str]:
|
||||
"""Parse --set and --merge options which may be passed individually or as a comma-separated list"""
|
||||
new_values = []
|
||||
for value in values:
|
||||
new_values.extend([v.strip() for v in value.split(",")])
|
||||
return tuple(new_values)
|
||||
|
||||
|
||||
def open_metadata_db(db_path: str):
|
||||
"""Open metadata database at db_path"""
|
||||
metadata_db = SQLiteKVStore(
|
||||
db_path,
|
||||
wal=False, # don't use WAL to keep database a single file
|
||||
)
|
||||
if not metadata_db.about:
|
||||
metadata_db.about = f"osxphotos metadata sync database\n{SYNC_ABOUT_STRING}"
|
||||
return metadata_db
|
||||
|
||||
|
||||
def key_from_photo(photo: PhotoInfo) -> str:
|
||||
"""Return key for photo used to correlate photos between libraries"""
|
||||
return f"{photo.fingerprint}:{photo.original_filename}"
|
||||
|
||||
|
||||
def get_photo_metadata(photos: list[PhotoInfo]) -> str:
|
||||
"""Return JSON string of metadata for photos; if more than one photo, merge metadata"""
|
||||
if len(photos) == 1:
|
||||
return photos[0].json()
|
||||
|
||||
# more than one photo with same fingerprint; merge metadata
|
||||
merge_fields = ["keywords", "persons", "albums", "title", "description", "uuid"]
|
||||
photos_dict = {}
|
||||
for photo in photos:
|
||||
data = photo.asdict()
|
||||
for k, v in data.items():
|
||||
if k not in photos_dict:
|
||||
photos_dict[k] = v.copy() if isinstance(v, (list, dict)) else v
|
||||
else:
|
||||
# merge data if it's a merge field
|
||||
if k in merge_fields and v:
|
||||
if isinstance(v, (list, tuple)):
|
||||
photos_dict[k] = sorted(list(set(photos_dict[k]) | set(v)))
|
||||
else:
|
||||
if v:
|
||||
if not photos_dict[k]:
|
||||
photos_dict[k] = v
|
||||
elif photos_dict[k] and v != photos_dict[k]:
|
||||
photos_dict[k] = f"{photos_dict[k]} {v}"
|
||||
# convert photos_dict to JSON string
|
||||
# wouldn't it be nice if json encoder handled datetimes...
|
||||
def default(o):
|
||||
if isinstance(o, (datetime.date, datetime.datetime)):
|
||||
return o.isoformat()
|
||||
|
||||
return json.dumps(photos_dict, sort_keys=True, default=default)
|
||||
|
||||
|
||||
def export_metadata(
|
||||
photos: list[PhotoInfo], output_path: str, verbose: Callable[..., None]
|
||||
):
|
||||
"""Export metadata to metadata_db"""
|
||||
metadata_db = open_metadata_db(output_path)
|
||||
verbose(f"Exporting metadata to [filepath]{output_path}[/]")
|
||||
num_photos = len(photos)
|
||||
photo_word = pluralize(num_photos, "photo", "photos")
|
||||
verbose(f"Analyzing [num]{num_photos}[/] {photo_word} to export")
|
||||
verbose(f"Exporting [num]{len(photos)}[/] {photo_word} to {output_path}")
|
||||
export_metadata_to_db(photos, metadata_db, progress=True)
|
||||
echo(
|
||||
f"Done: exported metadata for [num]{len(photos)}[/] {photo_word} to [filepath]{output_path}[/]"
|
||||
)
|
||||
metadata_db.close()
|
||||
|
||||
|
||||
def export_metadata_to_db(
|
||||
photos: list[PhotoInfo],
|
||||
metadata_db: SQLiteKVStore,
|
||||
progress: bool = True,
|
||||
):
|
||||
"""Export metadata for photos to metadata database
|
||||
|
||||
Args:
|
||||
photos: list of PhotoInfo objects
|
||||
metadata_db: SQLiteKVStore object
|
||||
progress: if True, show progress bar
|
||||
"""
|
||||
# it is possible to have multiple photos with the same fingerprint
|
||||
# for example, the same photo was imported twice or the photo was duplicated in Photos
|
||||
# in this case, we need to merge the metadata for the photos with the same fingerprint
|
||||
# as there is no way to know which photo is the "correct" one
|
||||
key_to_photos = {}
|
||||
for photo in photos:
|
||||
key = key_from_photo(photo)
|
||||
if key in key_to_photos:
|
||||
key_to_photos[key].append(photo)
|
||||
else:
|
||||
key_to_photos[key] = [photo]
|
||||
|
||||
with rich_progress(console=get_verbose_console(), mock=not progress) as progress:
|
||||
task = progress.add_task("Exporting metadata", total=len(key_to_photos))
|
||||
for key, key_photos in key_to_photos.items():
|
||||
metadata_db[key] = get_photo_metadata(key_photos)
|
||||
progress.advance(task)
|
||||
|
||||
|
||||
def get_import_type(import_path: str) -> Literal["library", "export"]:
|
||||
"""Determine if import_path is a Photos library, Photos database, or metadata export file"""
|
||||
if pathlib.Path(import_path).is_dir():
|
||||
if import_path.endswith(".photoslibrary"):
|
||||
return "library"
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import library: {import_path}"
|
||||
)
|
||||
else:
|
||||
# import_path is a file, need to determine if it's a Photos database or metadata export file
|
||||
try:
|
||||
get_db_version(import_path)
|
||||
except Exception as e:
|
||||
try:
|
||||
db = SQLiteKVStore(import_path)
|
||||
if db.about:
|
||||
return "export"
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import file: {import_path}"
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise ValueError(
|
||||
f"Unable to determine type of import file: {import_path}"
|
||||
) from e
|
||||
else:
|
||||
return "library"
|
||||
|
||||
|
||||
def import_metadata(
|
||||
photos: list[PhotoInfo],
|
||||
import_path: str,
|
||||
set_: tuple[str, ...],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
unmatched: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Import metadata from metadata_db"""
|
||||
import_type = get_import_type(import_path)
|
||||
photo_word = pluralize(len(photos), "photo", "photos")
|
||||
verbose(
|
||||
f"Importing metadata for [num]{len(photos)}[/] {photo_word} from [filepath]{import_path}[/]"
|
||||
)
|
||||
|
||||
# build mapping of key to photo
|
||||
key_to_photo = {}
|
||||
for photo in photos:
|
||||
key = key_from_photo(photo)
|
||||
if key in key_to_photo:
|
||||
key_to_photo[key].append(photo)
|
||||
else:
|
||||
key_to_photo[key] = [photo]
|
||||
|
||||
# find keys in import_path that match keys in photos
|
||||
if import_type == "library":
|
||||
# create an in memory database of the import library
|
||||
# so that the rest of the comparison code can be the same
|
||||
photosdb = PhotosDB(import_path, verbose=verbose)
|
||||
# filter out shared photos which don't have a fingerprint and
|
||||
# whose metadata can't be set
|
||||
photos = photosdb.query(QueryOptions(not_shared=True))
|
||||
import_db = SQLiteKVStore(":memory:")
|
||||
verbose(f"Loading metadata from import library: [filepath]{import_path}[/]")
|
||||
export_metadata_to_db(photos, import_db, progress=False)
|
||||
elif import_type == "export":
|
||||
import_db = open_metadata_db(import_path)
|
||||
else:
|
||||
echo_error(
|
||||
f"Unable to determine type of import file: [filepath]{import_path}[/]"
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
results = SyncResults()
|
||||
for key, key_photos in key_to_photo.items():
|
||||
if key in import_db:
|
||||
# import metadata from import_db
|
||||
for photo in key_photos:
|
||||
verbose(
|
||||
f"Importing metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/])"
|
||||
)
|
||||
metadata = import_db[key]
|
||||
results += import_metadata_for_photo(
|
||||
photo, metadata, set_, merge, dry_run, verbose
|
||||
)
|
||||
elif unmatched:
|
||||
# unable to find metadata for photo in import_db
|
||||
for photo in key_photos:
|
||||
echo(
|
||||
f"Unable to find metadata for [filename]{photo.original_filename}[/] ([uuid]{photo.uuid}[/]) in [filepath]{import_path}[/]"
|
||||
)
|
||||
|
||||
if unmatched:
|
||||
# find any keys in import_db that don't match keys in photos
|
||||
for key in import_db.keys():
|
||||
if key not in key_to_photo:
|
||||
echo(f"Unable to find [uuid]{key}[/] in selected photos.")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def import_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: str,
|
||||
set_: tuple[str, ...],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Update metadata for photo from metadata
|
||||
|
||||
Args:
|
||||
photo: PhotoInfo object
|
||||
metadata: metadata to import (JSON string)
|
||||
set_: tuple of metadata fields to set
|
||||
merge: tuple of metadata fields to merge
|
||||
dry_run: if True, don't actually update metadata
|
||||
verbose: verbose function
|
||||
"""
|
||||
# convert metadata to dict
|
||||
metadata = json.loads(metadata)
|
||||
|
||||
results = SyncResults()
|
||||
if "albums" in set_ or "albums" in merge:
|
||||
# behavior is the same for albums for set and merge:
|
||||
# add photo to any new albums but do not remove from existing albums
|
||||
results += _update_albums_for_photo(photo, metadata, dry_run, verbose)
|
||||
|
||||
results += _set_metadata_for_photo(photo, metadata, set_, dry_run, verbose)
|
||||
results += _merge_metadata_for_photo(photo, metadata, merge, dry_run, verbose)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def _update_albums_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Add photo to new albums if necessary"""
|
||||
# add photo to any new albums but do not remove from existing albums
|
||||
results = SyncResults()
|
||||
value = sorted(metadata["albums"])
|
||||
before = sorted(photo.albums)
|
||||
albums_to_add = set(value) - set(before)
|
||||
if not albums_to_add:
|
||||
verbose(f"\tNothing to do for albums", level=2)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
"albums",
|
||||
False,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
for album in albums_to_add:
|
||||
verbose(f"\tAdding to album [filepath]{album}[/]")
|
||||
if not dry_run:
|
||||
PhotosAlbum(album, verbose=lambda x: verbose(f"\t{x}"), rich=True).add(
|
||||
photo
|
||||
)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
"albums",
|
||||
True,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _set_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
set_: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Set metadata for photo"""
|
||||
results = SyncResults()
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
|
||||
for field in set_:
|
||||
if field == "albums":
|
||||
continue
|
||||
|
||||
value = metadata[field]
|
||||
before = getattr(photo, field)
|
||||
|
||||
if isinstance(value, list):
|
||||
value = sorted(value)
|
||||
if isinstance(before, list):
|
||||
before = sorted(before)
|
||||
|
||||
if value != before:
|
||||
verbose(f"\tSetting {field} to {value} from {before}")
|
||||
if not dry_run:
|
||||
set_photo_property(photo_, field, value)
|
||||
else:
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
value != before,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def _merge_metadata_for_photo(
|
||||
photo: PhotoInfo,
|
||||
metadata: dict[str, Any],
|
||||
merge: tuple[str, ...],
|
||||
dry_run: bool,
|
||||
verbose: Callable[..., None],
|
||||
) -> SyncResults:
|
||||
"""Merge metadata for photo"""
|
||||
results = SyncResults()
|
||||
photo_ = photoscript.Photo(photo.uuid)
|
||||
|
||||
for field in merge:
|
||||
if field == "albums":
|
||||
continue
|
||||
|
||||
value = metadata[field]
|
||||
before = getattr(photo, field)
|
||||
|
||||
if isinstance(value, list):
|
||||
value = sorted(value)
|
||||
if isinstance(before, list):
|
||||
before = sorted(before)
|
||||
|
||||
if value == before:
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
False,
|
||||
before,
|
||||
value,
|
||||
)
|
||||
continue
|
||||
|
||||
if isinstance(value, list) and isinstance(before, list):
|
||||
new_value = sorted(set(value + before))
|
||||
elif isinstance(before, bool):
|
||||
new_value = value or bool(before)
|
||||
elif isinstance(before, str):
|
||||
value = value or ""
|
||||
new_value = f"{before} {value}" if value and value not in before else before
|
||||
elif before is None:
|
||||
new_value = value
|
||||
else:
|
||||
echo_error(
|
||||
f"Unable to merge {field} for [filename]{photo.original_filename}[filename]"
|
||||
)
|
||||
raise click.Abort()
|
||||
|
||||
if new_value != before:
|
||||
verbose(f"\tMerging {field} to {new_value} from {before}")
|
||||
if not dry_run:
|
||||
set_photo_property(photo_, field, new_value)
|
||||
else:
|
||||
# Merge'd value might still be the same as original value
|
||||
# (e.g. if value is str and has previously been merged)
|
||||
verbose(f"\tNothing to do for {field}", level=2)
|
||||
|
||||
results.add_result(
|
||||
photo.uuid,
|
||||
photo.original_filename,
|
||||
photo.fingerprint,
|
||||
field,
|
||||
new_value != before,
|
||||
before,
|
||||
new_value,
|
||||
)
|
||||
return results
|
||||
|
||||
|
||||
def set_photo_property(photo: photoscript.Photo, property: str, value: Any):
|
||||
"""Set property on photo"""
|
||||
|
||||
# do some basic validation
|
||||
if property == "keywords" and not isinstance(value, list):
|
||||
raise ValueError(f"keywords must be a list, not {type(value)}")
|
||||
elif property in {"title", "description"} and not isinstance(value, str):
|
||||
raise ValueError(f"{property} must be a str, not {type(value)}")
|
||||
elif property == "favorite":
|
||||
value = bool(value)
|
||||
elif property not in {"title", "description", "favorite", "keywords"}:
|
||||
raise ValueError(f"Unknown property: {property}")
|
||||
setattr(photo, property, value)
|
||||
|
||||
|
||||
def print_import_summary(results: SyncResults):
|
||||
"""Print summary of import results"""
|
||||
summary = results.results_summary()
|
||||
property_summary = ", ".join(
|
||||
f"updated {property}: [num]{summary.get(property,0)}[/]"
|
||||
for property in SYNC_PROPERTIES
|
||||
)
|
||||
echo(
|
||||
f"Processed [num]{summary['total']}[/] photos, updated: [num]{summary['updated']}[/], {property_summary}"
|
||||
)
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"--export",
|
||||
"-e",
|
||||
"export_path",
|
||||
metavar="EXPORT_FILE",
|
||||
help="Export metadata to file EXPORT_FILE for later use with --import. "
|
||||
"The export file will be a SQLite database; it is recommended to use the "
|
||||
".db extension though this is not required.",
|
||||
type=click.Path(dir_okay=False, writable=True),
|
||||
)
|
||||
@click.option(
|
||||
"--import",
|
||||
"-i",
|
||||
"import_path",
|
||||
metavar="IMPORT_PATH",
|
||||
help="Import metadata from file IMPORT_PATH. "
|
||||
"IMPORT_PATH can a Photos library, a Photos database, or a metadata export file "
|
||||
"created with --export.",
|
||||
type=SyncImportPath(),
|
||||
)
|
||||
@click.option(
|
||||
"--set",
|
||||
"-s",
|
||||
"set_",
|
||||
metavar="METADATA",
|
||||
multiple=True,
|
||||
help="When used with --import, set metadata in local Photos library to match import data. "
|
||||
"Multiple metadata properties can be specified by repeating the --set option "
|
||||
"or by using a comma-separated list. "
|
||||
f"METADATA can be one of: {', '.join(SYNC_IMPORT_TYPES_ALL)}. "
|
||||
"For example, to set keywords and favorite, use `--set keywords --set favorite` "
|
||||
"or `--set keywords,favorite`. "
|
||||
"If `--set all` is specified, all metadata will be set. "
|
||||
"Note that using --set overwrites any existing metadata in the local Photos library. "
|
||||
"For example, if a photo is marked as favorite in the local library but not in the import source, "
|
||||
"--set favorite will clear the favorite status in the local library. "
|
||||
"The exception to this is that `--set album` will not remove the photo "
|
||||
"from any existing albums in the local library but will add the photo to any new albums specified "
|
||||
"in the import source."
|
||||
"See also --merge.",
|
||||
type=SyncImportType(),
|
||||
)
|
||||
@click.option(
|
||||
"--merge",
|
||||
"-m",
|
||||
"merge",
|
||||
metavar="METADATA",
|
||||
multiple=True,
|
||||
help="When used with --import, merge metadata in local Photos library with import data. "
|
||||
"Multiple metadata properties can be specified by repeating the --merge option "
|
||||
"or by using a comma-separated list. "
|
||||
f"METADATA can be one of: {', '.join(SYNC_IMPORT_TYPES_ALL)}. "
|
||||
"For example, to merge keywords and favorite, use `--merge keywords --merge favorite` "
|
||||
"or `--merge keywords,favorite`. "
|
||||
"If `--merge all` is specified, all metadata will be merged. "
|
||||
"Note that using --merge does not overwrite any existing metadata in the local Photos library. "
|
||||
"For example, if a photo is marked as favorite in the local library but not in the import source, "
|
||||
"--merge favorite will not change the favorite status in the local library. "
|
||||
"See also --set.",
|
||||
type=SyncImportType(),
|
||||
)
|
||||
@click.option(
|
||||
"--unmatched",
|
||||
"-U",
|
||||
is_flag=True,
|
||||
help="When used with --import, print out a list of photos in the import source that "
|
||||
"were not matched against the local library. Also prints out a list of photos "
|
||||
"in the local library that were not matched against the import source. ",
|
||||
)
|
||||
@click.option(
|
||||
"--report",
|
||||
"-R",
|
||||
metavar="REPORT_FILE",
|
||||
help="Write a report of all photos that were processed with --import. "
|
||||
"The extension of the report filename will be used to determine the format. "
|
||||
"Valid extensions are: "
|
||||
".csv (CSV file), .json (JSON), .db and .sqlite (SQLite database). "
|
||||
"REPORT_FILE may be a an osxphotos template string, for example, "
|
||||
"--report 'update_{today.date}.csv' will write a CSV report file named with today's date. "
|
||||
"See also --append.",
|
||||
type=TemplateString(),
|
||||
)
|
||||
@click.option(
|
||||
"--append",
|
||||
"-A",
|
||||
is_flag=True,
|
||||
help="If used with --report, add data to existing report file instead of overwriting it. "
|
||||
"See also --report.",
|
||||
)
|
||||
@click.option(
|
||||
"--dry-run",
|
||||
is_flag=True,
|
||||
help="Dry run; " "when used with --import, don't actually update metadata.",
|
||||
)
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@QUERY_OPTIONS(exclude=["--shared", "--not-shared"])
|
||||
@DB_OPTION
|
||||
@THEME_OPTION
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def sync(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
append,
|
||||
dry_run,
|
||||
export_path,
|
||||
import_path,
|
||||
merge,
|
||||
report,
|
||||
set_,
|
||||
theme,
|
||||
timestamp,
|
||||
unmatched,
|
||||
verbose_flag,
|
||||
**kwargs, # query options
|
||||
):
|
||||
"""Sync metadata and albums between Photos libraries.
|
||||
|
||||
Use sync to update metadata in a local Photos library to match
|
||||
metadata in another Photos library. The sync command works by
|
||||
finding identical photos in the local library and the import source
|
||||
and then updating the metadata in the local library to match the
|
||||
metadata in the import source. Photos are considered identical if
|
||||
their original filename and fingerprint match.
|
||||
|
||||
The import source can be a Photos library or a metadata export file
|
||||
created with the --export option.
|
||||
|
||||
The sync command can be useful if you have imported the same photos to
|
||||
multiple Photos libraries and want to keep the metadata in all libraries
|
||||
in sync.
|
||||
|
||||
Metadata can be overwritten (--set) or merged (--merge) with the metadata
|
||||
in the import source. You may specify specific metadata to sync or sync
|
||||
all metadata. See --set and --merge for more details.
|
||||
|
||||
The sync command can be used to sync metadata between an iPhone or iPad
|
||||
and a Mac, for example, in the case where you do not use iCloud but
|
||||
manually import photos from your iPhone or iPad to your Mac. To do this,
|
||||
you'll first need to copy the Photos database from the iPhone or iPad to
|
||||
your Mac. This can be done by connecting your iPhone or iPad to your Mac
|
||||
using a USB cable and then copying the Photos database from the iPhone
|
||||
using a third-party tool such as iMazing (https://imazing.com/). You can
|
||||
then use the sync command and set the import source to the Photos database
|
||||
you copied from the iPhone or iPad.
|
||||
|
||||
The sync command can also be used to sync metadata between users using
|
||||
iCloud Shared Photo Library. NOTE: This use case has not yet been
|
||||
tested. If you use iCloud Shared Photo Library and would like to help
|
||||
test this use case, please connect with me on GitHub:
|
||||
https://github.com/RhetTbull/osxphotos/issues/887
|
||||
|
||||
You can run the --export and --import commands together. In this case,
|
||||
the import will be run first and then the export will be run.
|
||||
|
||||
For example, if you want to sync two Photos libraries between users or
|
||||
two different computers, you can export the metadata to a shared folder.
|
||||
|
||||
On the first computer, run:
|
||||
|
||||
osxphotos sync --export /path/to/export/folder/computer1.db --merge all --import /path/to/export/folder/computer2.db
|
||||
|
||||
On the second computer, run:
|
||||
|
||||
osxphotos sync --export /path/to/export/folder/computer2.db --merge all --import /path/to/export/folder/computer1.db
|
||||
|
||||
"""
|
||||
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if (set_ or merge) and not import_path:
|
||||
echo_error("--set and --merge can only be used with --import")
|
||||
ctx.exit(1)
|
||||
|
||||
# filter out photos in shared albums as these cannot be updated
|
||||
kwargs["not_shared"] = True
|
||||
|
||||
set_ = parse_set_merge(set_)
|
||||
merge = parse_set_merge(merge)
|
||||
|
||||
if "all" in set_:
|
||||
set_ = tuple(SYNC_IMPORT_TYPES)
|
||||
if "all" in merge:
|
||||
merge = tuple(SYNC_IMPORT_TYPES)
|
||||
|
||||
if set_ and merge:
|
||||
# fields in set cannot be in merge and vice versa
|
||||
set_ = set(set_)
|
||||
merge = set(merge)
|
||||
if set_ & merge:
|
||||
echo_error(
|
||||
"--set and --merge cannot be used with the same fields: "
|
||||
f"set: {set_}, merge: {merge}"
|
||||
)
|
||||
ctx.exit(1)
|
||||
|
||||
if import_path:
|
||||
try:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
except IncompatibleQueryOptions:
|
||||
echo_error("Incompatible query options")
|
||||
echo_error(ctx.obj.group.commands["repl"].get_help(ctx))
|
||||
ctx.exit(1)
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
photos = photosdb.query(query_options)
|
||||
results = import_metadata(
|
||||
photos, import_path, set_, merge, dry_run, unmatched, verbose
|
||||
)
|
||||
if report:
|
||||
report_path = render_and_validate_report(report)
|
||||
verbose(f"Writing report to {report_path}")
|
||||
report_writer = sync_report_writer_factory(report_path, append=append)
|
||||
report_writer.write(results)
|
||||
report_writer.close()
|
||||
print_import_summary(results)
|
||||
|
||||
if export_path:
|
||||
query_options = query_options_from_kwargs(**kwargs)
|
||||
photosdb = PhotosDB(dbfile=db, verbose=verbose)
|
||||
photos = photosdb.query(query_options)
|
||||
export_metadata(photos, export_path, verbose)
|
||||
175
osxphotos/cli/sync_results.py
Normal file
175
osxphotos/cli/sync_results.py
Normal file
@@ -0,0 +1,175 @@
|
||||
"""SyncResults class for osxphotos sync command"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import json
|
||||
|
||||
from osxphotos.photoinfo import PhotoInfo
|
||||
|
||||
SYNC_PROPERTIES = [
|
||||
"albums",
|
||||
"description",
|
||||
"favorite",
|
||||
"keywords",
|
||||
"title",
|
||||
]
|
||||
|
||||
|
||||
class SyncResults:
|
||||
"""Results of sync set/merge"""
|
||||
|
||||
def __init__(self):
|
||||
self._results = {}
|
||||
self._datetime = datetime.datetime.now()
|
||||
|
||||
def add_result(
|
||||
self,
|
||||
uuid: str,
|
||||
filename: str,
|
||||
fingerprint: str,
|
||||
property: str,
|
||||
updated: bool,
|
||||
before: str | list[str] | bool | None,
|
||||
after: str | list[str] | bool | None,
|
||||
):
|
||||
"""Add result for a single photo"""
|
||||
if uuid not in self._results:
|
||||
self._results[uuid] = {
|
||||
"filename": filename,
|
||||
"fingerprint": fingerprint,
|
||||
"properties": {
|
||||
property: {
|
||||
"updated": updated,
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"before": before,
|
||||
"after": after,
|
||||
},
|
||||
},
|
||||
}
|
||||
else:
|
||||
self._results[uuid]["properties"][property] = {
|
||||
"updated": updated,
|
||||
"datetime": datetime.datetime.now().isoformat(),
|
||||
"before": before,
|
||||
"after": after,
|
||||
}
|
||||
|
||||
@property
|
||||
def results(self):
|
||||
"""Return results"""
|
||||
return self._results
|
||||
|
||||
@property
|
||||
def results_list(self):
|
||||
"""Return results as list lists where each sublist is values for a single photo"""
|
||||
results = []
|
||||
for uuid, record in self._results.items():
|
||||
row = [
|
||||
uuid,
|
||||
record["filename"],
|
||||
record["fingerprint"],
|
||||
self._any_updated(uuid),
|
||||
]
|
||||
for property in SYNC_PROPERTIES:
|
||||
if property in record["properties"]:
|
||||
row.extend(
|
||||
record["properties"][property][column]
|
||||
for column in ["updated", "datetime", "before", "after"]
|
||||
)
|
||||
else:
|
||||
row.extend([False, "", "", ""])
|
||||
results.append(row)
|
||||
return results
|
||||
|
||||
@property
|
||||
def results_header(self):
|
||||
"""Return headers for results_list"""
|
||||
header = ["uuid", "filename", "fingerprint", "updated"]
|
||||
for property in SYNC_PROPERTIES:
|
||||
header.extend(
|
||||
f"{property}_{column}"
|
||||
for column in ["updated", "datetime", "before", "after"]
|
||||
)
|
||||
return header
|
||||
|
||||
@property
|
||||
def results_dict(self):
|
||||
"""Return dictionary of results"""
|
||||
results = {}
|
||||
for uuid, record in self._results.items():
|
||||
results[uuid] = {
|
||||
"uuid": uuid,
|
||||
"filename": record["filename"],
|
||||
"fingerprint": record["fingerprint"],
|
||||
"updated": self._any_updated(uuid),
|
||||
}
|
||||
for property in SYNC_PROPERTIES:
|
||||
if property in record["properties"]:
|
||||
results[uuid][property] = record["properties"][property]
|
||||
else:
|
||||
results[uuid][property] = {
|
||||
"updated": False,
|
||||
"datetime": None,
|
||||
"before": None,
|
||||
"after": None,
|
||||
}
|
||||
return results
|
||||
|
||||
def results_summary(self):
|
||||
"""Get summary of results"""
|
||||
updated = sum(bool(self._any_updated(uuid)) for uuid in self._results.keys())
|
||||
property_updated = {}
|
||||
for property in SYNC_PROPERTIES:
|
||||
property_updated[property] = 0
|
||||
for uuid in self._results.keys():
|
||||
if self._results[uuid]["properties"].get(property, {"updated": False})[
|
||||
"updated"
|
||||
]:
|
||||
property_updated[property] += 1
|
||||
return {
|
||||
"total": len(self._results),
|
||||
"updated": updated,
|
||||
} | property_updated
|
||||
|
||||
def _any_updated(self, uuid: str) -> bool:
|
||||
"""Return True if any property was updated for this photo"""
|
||||
return any(
|
||||
self._results[uuid]["properties"].get(property, {"updated": False})[
|
||||
"updated"
|
||||
]
|
||||
for property in SYNC_PROPERTIES
|
||||
)
|
||||
|
||||
def __add__(self, other):
|
||||
"""Add results from another SyncResults"""
|
||||
for uuid in other._results.keys():
|
||||
for property, values in other._results[uuid]["properties"].items():
|
||||
self.add_result(
|
||||
uuid,
|
||||
other._results[uuid]["filename"],
|
||||
other._results[uuid]["fingerprint"],
|
||||
property,
|
||||
values["updated"],
|
||||
values["before"],
|
||||
values["after"],
|
||||
)
|
||||
return self
|
||||
|
||||
def __iadd__(self, other):
|
||||
"""Add results from another SyncResults"""
|
||||
for uuid in other._results.keys():
|
||||
for property, values in other._results[uuid]["properties"].items():
|
||||
self.add_result(
|
||||
uuid,
|
||||
other._results[uuid]["filename"],
|
||||
other._results[uuid]["fingerprint"],
|
||||
property,
|
||||
values["updated"],
|
||||
values["before"],
|
||||
values["after"],
|
||||
)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self._results, indent=2)
|
||||
@@ -1,43 +1,44 @@
|
||||
""" Fix time / date / timezone for photos in Apple Photos """
|
||||
|
||||
import datetime
|
||||
import os
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from functools import partial
|
||||
from textwrap import dedent
|
||||
from typing import Callable, Optional
|
||||
|
||||
import click
|
||||
from photoscript import Photo, PhotosLibrary
|
||||
from photoscript import PhotosLibrary
|
||||
from rich.console import Console
|
||||
|
||||
from osxphotos._constants import APP_NAME
|
||||
from osxphotos._version import __version__
|
||||
from osxphotos.compare_exif import PhotoCompare
|
||||
from osxphotos.crash_reporter import crash_reporter, set_crash_data
|
||||
from osxphotos.datetime_utils import datetime_naive_to_local, datetime_to_new_tz
|
||||
from osxphotos.exif_datetime_updater import ExifDateTimeUpdater
|
||||
from osxphotos.exiftool import get_exiftool_path
|
||||
from osxphotos.photodates import (
|
||||
set_photo_date_from_filename,
|
||||
update_photo_date_time,
|
||||
update_photo_from_function,
|
||||
update_photo_time_for_new_timezone,
|
||||
)
|
||||
from osxphotos.photosalbum import PhotosAlbumPhotoScript
|
||||
from osxphotos.phototz import PhotoTimeZone, PhotoTimeZoneUpdater
|
||||
from osxphotos.timeutils import update_datetime
|
||||
from osxphotos.timezones import Timezone
|
||||
from osxphotos.utils import noop, pluralize
|
||||
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
rich_echo,
|
||||
rich_echo_error,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .cli_params import THEME_OPTION, TIMESTAMP_OPTION, VERBOSE_OPTION
|
||||
from .click_rich_echo import rich_click_echo as echo
|
||||
from .click_rich_echo import rich_echo_error as echo_error
|
||||
from .color_themes import get_theme
|
||||
from .common import THEME_OPTION
|
||||
from .common import OSXPHOTOS_CRASH_LOG
|
||||
from .darkmode import is_dark_mode
|
||||
from .help import HELP_WIDTH, rich_text
|
||||
from .param_types import (
|
||||
DateOffset,
|
||||
DateTimeISO8601,
|
||||
FunctionCall,
|
||||
StrpDateTimePattern,
|
||||
TimeOffset,
|
||||
TimeString,
|
||||
UTCOffset,
|
||||
@@ -49,110 +50,6 @@ from .verbose import get_verbose_console, verbose_print
|
||||
DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S%z"
|
||||
|
||||
|
||||
def update_photo_date_time(
|
||||
photo: Photo,
|
||||
date,
|
||||
time,
|
||||
date_delta,
|
||||
time_delta,
|
||||
verbose_print: Callable,
|
||||
):
|
||||
"""Update date, time in photo"""
|
||||
photo_date = photo.date
|
||||
new_photo_date = update_datetime(
|
||||
photo_date, date=date, time=time, date_delta=date_delta, time_delta=time_delta
|
||||
)
|
||||
filename = photo.filename
|
||||
uuid = photo.uuid
|
||||
if new_photo_date != photo_date:
|
||||
photo.date = new_photo_date
|
||||
verbose_print(
|
||||
f"Updated date/time for photo [filename]{filename}[/filename] "
|
||||
f"([uuid]{uuid}[/uuid]) from: [time]{photo_date}[/time] to [time]{new_photo_date}[/time]"
|
||||
)
|
||||
else:
|
||||
verbose_print(
|
||||
f"Skipped date/time update for photo [filename]{filename}[/filename] "
|
||||
f"([uuid]{uuid}[/uuid]): nothing to do"
|
||||
)
|
||||
|
||||
|
||||
def update_photo_time_for_new_timezone(
|
||||
library_path: str,
|
||||
photo: Photo,
|
||||
new_timezone: Timezone,
|
||||
verbose_print: Callable,
|
||||
):
|
||||
"""Update time in photo to keep it the same time but in a new timezone
|
||||
|
||||
For example, photo time is 12:00+0100 and new timezone is +0200,
|
||||
so adjust photo time by 1 hour so it will now be 12:00+0200 instead of
|
||||
13:00+0200 as it would be with no adjustment to the time"""
|
||||
old_timezone = PhotoTimeZone(library_path=library_path).get_timezone(photo)[0]
|
||||
# need to move time in opposite direction of timezone offset so that
|
||||
# photo time is the same time but in the new timezone
|
||||
delta = old_timezone - new_timezone.offset
|
||||
photo_date = photo.date
|
||||
new_photo_date = update_datetime(
|
||||
dt=photo_date, time_delta=datetime.timedelta(seconds=delta)
|
||||
)
|
||||
filename = photo.filename
|
||||
uuid = photo.uuid
|
||||
if photo_date != new_photo_date:
|
||||
photo.date = new_photo_date
|
||||
verbose_print(
|
||||
f"Adjusted date/time for photo [filename]{filename}[/filename] ([uuid]{uuid}[/uuid]) to match "
|
||||
f"previous time [time]{photo_date}[time] but in new timezone [tz]{new_timezone}[/tz]."
|
||||
)
|
||||
else:
|
||||
verbose_print(
|
||||
f"Skipping date/time update for photo [filename]{filename}[/filename] ([uuid]{photo.uuid}[/uuid]), "
|
||||
f"already matches new timezone [tz]{new_timezone}[/tz]"
|
||||
)
|
||||
|
||||
|
||||
def update_photo_from_function(
|
||||
library_path: str,
|
||||
function: Callable,
|
||||
verbose_print: Callable,
|
||||
photo: Photo,
|
||||
path: Optional[str],
|
||||
):
|
||||
"""Update photo from function call"""
|
||||
photo_tz_sec, _, photo_tz_name = PhotoTimeZone(
|
||||
library_path=library_path
|
||||
).get_timezone(photo)
|
||||
dt_new, tz_new = function(
|
||||
photo=photo,
|
||||
path=path,
|
||||
tz_sec=photo_tz_sec,
|
||||
tz_name=photo_tz_name,
|
||||
verbose=verbose_print,
|
||||
)
|
||||
if dt_new != photo.date:
|
||||
old_date = photo.date
|
||||
photo.date = dt_new
|
||||
verbose_print(
|
||||
f"Updated date/time for photo [filename]{photo.filename}[/filename] "
|
||||
f"([uuid]{photo.uuid}[/uuid]) from: [time]{old_date}[/time] to [time]{dt_new}[/time]"
|
||||
)
|
||||
else:
|
||||
verbose_print(
|
||||
f"Skipped date/time update for photo [filename]{photo.filename}[/filename] "
|
||||
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
|
||||
)
|
||||
if tz_new != photo_tz_sec:
|
||||
tz_updater = PhotoTimeZoneUpdater(
|
||||
timezone=Timezone(tz_new), verbose=verbose_print, library_path=library_path
|
||||
)
|
||||
tz_updater.update_photo(photo)
|
||||
else:
|
||||
verbose_print(
|
||||
f"Skipped timezone update for photo [filename]{photo.filename}[/filename] "
|
||||
f"([uuid]{photo.uuid}[/uuid]): nothing to do"
|
||||
)
|
||||
|
||||
|
||||
class TimeWarpCommand(click.Command):
|
||||
"""Custom click.Command that overrides get_help() to show additional help info for export"""
|
||||
|
||||
@@ -223,6 +120,40 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
|
||||
|
||||
`osxphotos timewarp --pull-exif --use-file-time --verbose`
|
||||
|
||||
## Parsing Dates/Times from Filenames
|
||||
|
||||
The --parse-date option allows you to parse dates/times from the original filename of the photo.
|
||||
This is useful if you files with dates/times embedded in the filename but not in the metadata.
|
||||
|
||||
|
||||
The argument to `--parse-date` is a pattern string that is used to parse the date/time
|
||||
from the filename. The pattern string is a superset of the python `strftime/strptime`
|
||||
format with the following additions:
|
||||
|
||||
- *: Match any number of characters
|
||||
- ^: Match the beginning of the string
|
||||
- $: Match the end of the string
|
||||
- {n}: Match exactly n characters
|
||||
- {n,}: Match at least n characters
|
||||
- {n,m}: Match at least n characters and at most m characters
|
||||
- In addition to `%%` for a literal `%`, the following format codes are supported:
|
||||
`%^`, `%$`, `%*`, `%|`, `%{`, `%}` for `^`, `$`, `*`, `|`, `{`, `}` respectively
|
||||
- |: join multiple format codes; each code is tried in order until one matches
|
||||
- Unlike the standard library, the leading zero is not optional for
|
||||
%d, %m, %H, %I, %M, %S, %j, %U, %W, and %V
|
||||
- For optional leading zero, use %-d, %-m, %-H, %-I, %-M, %-S, %-j, %-U, %-W, and %-V
|
||||
|
||||
For more information on strptime format codes, see:
|
||||
https://docs.python.org/3/library/datetime.html?highlight=strptime#strftime-and-strptime-format-codes
|
||||
|
||||
**Note**: The time zone of the parsed date/time is assumed to be the local time zone.
|
||||
If the parse pattern includes a time zone, the photo's time will be converted from
|
||||
the specified time zone to the local time zone. osxphotos import does not
|
||||
currently support setting the time zone of imported photos.
|
||||
See also `osxphotos help timewarp` for more information on the timewarp
|
||||
command which can be used to change the time zone of photos after import.
|
||||
|
||||
|
||||
"""
|
||||
),
|
||||
width=formatter.width,
|
||||
@@ -322,6 +253,19 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
|
||||
"Requires the third-party exiftool utility be installed (see https://exiftool.org/). "
|
||||
"See also --push-exif.",
|
||||
)
|
||||
@click.option(
|
||||
"--parse-date",
|
||||
"-M",
|
||||
metavar="DATE_PATTERN",
|
||||
type=StrpDateTimePattern(),
|
||||
help="Parse date from filename using DATE_PATTERN and set photo's date to match. "
|
||||
"If file does not match DATE_PATTERN, the date will not be changed. "
|
||||
"DATE_PATTERN is a strptime-compatible pattern with extensions as pattern described below. "
|
||||
"If DATE_PATTERN matches time zone information, the photo's timezone will be set to match. "
|
||||
"For example, if your photos are named 'IMG_1234_2022_11_23_12_34_56.jpg' where the date/time is "
|
||||
"'2022-11-23 12:34:56', you could use the pattern '%Y_%m_%d_%H_%M_%S' or "
|
||||
"'IMG_*_%Y_%m_%d_%H_%M_%S' to further narrow the pattern to only match files with 'IMG_xxxx_' in the name.",
|
||||
)
|
||||
@click.option(
|
||||
"--function",
|
||||
"-F",
|
||||
@@ -363,7 +307,8 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
|
||||
help="When used with --compare-exif, adds any photos with date/time/timezone differences "
|
||||
"between Photos/EXIF to album ALBUM. If ALBUM does not exist, it will be created.",
|
||||
)
|
||||
@click.option("--verbose", "-V", "verbose", is_flag=True, help="Show verbose output.")
|
||||
@VERBOSE_OPTION
|
||||
@TIMESTAMP_OPTION
|
||||
@click.option(
|
||||
"--library",
|
||||
"-L",
|
||||
@@ -379,19 +324,6 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
|
||||
type=click.Path(exists=True),
|
||||
help="Optional path to exiftool executable (will look in $PATH if not specified) for those options which require exiftool.",
|
||||
)
|
||||
@click.option(
|
||||
"--output-file",
|
||||
"-o",
|
||||
type=click.File(mode="w", lazy=False),
|
||||
help="Output file. If not specified, output is written to stdout.",
|
||||
)
|
||||
@click.option(
|
||||
"--terminal-width",
|
||||
"-w",
|
||||
type=int,
|
||||
help="Terminal width in characters.",
|
||||
hidden=True,
|
||||
)
|
||||
@click.option("--timestamp", is_flag=True, help="Add time stamp to verbose output")
|
||||
@THEME_OPTION
|
||||
@click.option(
|
||||
@@ -404,6 +336,13 @@ if the EXIF data is missing, use the file modification date/time; show verbose o
|
||||
is_flag=True,
|
||||
help="Bypass confirmation prompt. Use with caution.",
|
||||
)
|
||||
@crash_reporter(
|
||||
OSXPHOTOS_CRASH_LOG,
|
||||
"[red]Something went wrong and osxphotos encountered an error:[/red]",
|
||||
"osxphotos crash log",
|
||||
"Please file a bug report at https://github.com/RhetTbull/osxphotos/issues with the crash log attached.",
|
||||
f"osxphotos version: {__version__}",
|
||||
)
|
||||
def timewarp(
|
||||
date,
|
||||
date_delta,
|
||||
@@ -419,12 +358,11 @@ def timewarp(
|
||||
use_file_time,
|
||||
add_to_album,
|
||||
exiftool_path,
|
||||
verbose,
|
||||
verbose_flag,
|
||||
library,
|
||||
theme,
|
||||
parse_date,
|
||||
plain,
|
||||
output_file,
|
||||
terminal_width,
|
||||
timestamp,
|
||||
force,
|
||||
):
|
||||
@@ -436,6 +374,8 @@ def timewarp(
|
||||
See Timewarp Overview below for additional information.
|
||||
"""
|
||||
|
||||
set_crash_data("locals", locals())
|
||||
|
||||
# check constraints
|
||||
if not any(
|
||||
[
|
||||
@@ -446,6 +386,7 @@ def timewarp(
|
||||
timezone,
|
||||
inspect,
|
||||
compare_exif,
|
||||
parse_date,
|
||||
push_exif,
|
||||
pull_exif,
|
||||
function,
|
||||
@@ -453,7 +394,8 @@ def timewarp(
|
||||
):
|
||||
raise click.UsageError(
|
||||
"At least one of --date, --date-delta, --time, --time-delta, "
|
||||
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, --function "
|
||||
"--timezone, --inspect, --compare-exif, --push-exif, --pull-exif, "
|
||||
"--parse-date, --function "
|
||||
"must be specified."
|
||||
)
|
||||
|
||||
@@ -469,54 +411,28 @@ def timewarp(
|
||||
if add_to_album and not compare_exif:
|
||||
raise click.UsageError("--add-to-album must be used with --compare-exif.")
|
||||
|
||||
# configure colored rich output
|
||||
# TODO: this is all a little hacky, find a better way to do this
|
||||
color_theme = get_theme(theme)
|
||||
verbose_ = verbose_print(
|
||||
verbose,
|
||||
timestamp,
|
||||
rich=True,
|
||||
theme=color_theme,
|
||||
highlight=False,
|
||||
file=output_file,
|
||||
)
|
||||
# set console for rich_echo to be same as for verbose_
|
||||
terminal_width = terminal_width or (1000 if output_file else None)
|
||||
if output_file:
|
||||
set_rich_console(Console(file=output_file, width=terminal_width))
|
||||
elif terminal_width:
|
||||
set_rich_console(
|
||||
Console(
|
||||
file=sys.stdout,
|
||||
theme=color_theme,
|
||||
force_terminal=True,
|
||||
width=terminal_width,
|
||||
)
|
||||
)
|
||||
else:
|
||||
set_rich_console(get_verbose_console(theme=color_theme))
|
||||
set_rich_theme(color_theme)
|
||||
verbose = verbose_print(verbose=verbose_flag, timestamp=timestamp, theme=theme)
|
||||
|
||||
if any([compare_exif, push_exif, pull_exif]):
|
||||
exiftool_path = exiftool_path or get_exiftool_path()
|
||||
verbose_(f"exiftool path: [filename]{exiftool_path}[/filename]")
|
||||
verbose(f"exiftool path: [filename]{exiftool_path}[/filename]")
|
||||
|
||||
try:
|
||||
photos = PhotosLibrary().selection
|
||||
if not photos:
|
||||
rich_echo_error("[warning]No photos selected[/]")
|
||||
echo_error("[warning]No photos selected[/]")
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
# AppleScript error -1728 occurs if user attempts to get selected photos in a Smart Album
|
||||
if "(-1728)" in str(e):
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
"[error]Could not get selected photos. Ensure photos is open and photos are selected. "
|
||||
"If you have selected photos and you see this message, it may be because the selected photos are in a Photos Smart Album. "
|
||||
f"{APP_NAME} cannot access photos in a Smart Album. Select the photos in a regular album or in 'All Photos' view. "
|
||||
"Another option is to create a new album using 'File | New Album With Selection' then select the photos in the new album.[/]",
|
||||
)
|
||||
else:
|
||||
rich_echo_error(
|
||||
echo_error(
|
||||
f"[error]Could not get selected photos. Ensure Photos is open and photos to process are selected. {e}[/]",
|
||||
)
|
||||
sys.exit(1)
|
||||
@@ -533,6 +449,7 @@ def timewarp(
|
||||
push_exif,
|
||||
pull_exif,
|
||||
function,
|
||||
parse_date,
|
||||
]
|
||||
)
|
||||
and not force
|
||||
@@ -556,13 +473,19 @@ def timewarp(
|
||||
time=time,
|
||||
date_delta=date_delta,
|
||||
time_delta=time_delta,
|
||||
verbose_print=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
update_photo_time_for_new_timezone_ = partial(
|
||||
update_photo_time_for_new_timezone,
|
||||
library_path=library,
|
||||
verbose_print=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
set_photo_date_from_filename_ = partial(
|
||||
set_photo_date_from_filename,
|
||||
library_path=library,
|
||||
verbose=verbose,
|
||||
)
|
||||
|
||||
if function:
|
||||
@@ -570,7 +493,7 @@ def timewarp(
|
||||
update_photo_from_function,
|
||||
library_path=library,
|
||||
function=function[0],
|
||||
verbose_print=verbose_,
|
||||
verbose=verbose,
|
||||
)
|
||||
else:
|
||||
update_photo_from_function_ = noop
|
||||
@@ -578,15 +501,22 @@ def timewarp(
|
||||
if inspect:
|
||||
tzinfo = PhotoTimeZone(library_path=library)
|
||||
if photos:
|
||||
rich_echo(
|
||||
"[filename]filename[/filename], [uuid]uuid[/uuid], [time]photo time (local)[/time], [time]photo time[/time], [tz]timezone offset[/tz], [tz]timezone name[/tz]"
|
||||
echo(
|
||||
"[filename]filename[/filename], [uuid]uuid[/uuid], "
|
||||
"[time]photo time (local)[/time], "
|
||||
"[time]photo time[/time], "
|
||||
"[tz]timezone offset[/tz], [tz]timezone name[/tz]"
|
||||
)
|
||||
for photo in photos:
|
||||
set_crash_data("photo", f"{photo.uuid} {photo.filename}")
|
||||
tz_seconds, tz_str, tz_name = tzinfo.get_timezone(photo)
|
||||
photo_date_local = datetime_naive_to_local(photo.date)
|
||||
photo_date_tz = datetime_to_new_tz(photo_date_local, tz_seconds)
|
||||
rich_echo(
|
||||
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], [time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], [time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], [tz]{tz_str}[/tz], [tz]{tz_name}[/tz]"
|
||||
echo(
|
||||
f"[filename]{photo.filename}[/filename], [uuid]{photo.uuid}[/uuid], "
|
||||
f"[time]{photo_date_local.strftime(DATETIME_FORMAT)}[/time], "
|
||||
f"[time]{photo_date_tz.strftime(DATETIME_FORMAT)}[/time], "
|
||||
f"[tz]{tz_str}[/tz], [tz]{tz_name}[/tz]"
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -596,14 +526,15 @@ def timewarp(
|
||||
if photos:
|
||||
photocomp = PhotoCompare(
|
||||
library_path=library,
|
||||
verbose=verbose_,
|
||||
verbose=verbose,
|
||||
exiftool_path=exiftool_path,
|
||||
)
|
||||
if not album:
|
||||
rich_echo(
|
||||
echo(
|
||||
"filename, uuid, photo time (Photos), photo time (EXIF), timezone offset (Photos), timezone offset (EXIF)"
|
||||
)
|
||||
for photo in photos:
|
||||
set_crash_data("photo", f"{photo.uuid} {photo.filename}")
|
||||
diff_results = (
|
||||
photocomp.compare_exif_no_markup(photo)
|
||||
if plain
|
||||
@@ -622,20 +553,20 @@ def timewarp(
|
||||
if album:
|
||||
if diff_results.diff:
|
||||
different_photos += 1
|
||||
verbose_(
|
||||
verbose(
|
||||
f"Photo {filename} ({uuid}) has different date/time/timezone, adding to album '{album.name}'"
|
||||
)
|
||||
album.add(photo)
|
||||
else:
|
||||
verbose_(f"Photo {filename} ({uuid}) has same date/time/timezone")
|
||||
verbose(f"Photo {filename} ({uuid}) has same date/time/timezone")
|
||||
else:
|
||||
rich_echo(
|
||||
echo(
|
||||
f"{filename}, {uuid}, "
|
||||
f"{diff_results.photos_date} {diff_results.photos_time}, {diff_results.exif_date} {diff_results.exif_time}, "
|
||||
f"{diff_results.photos_tz}, {diff_results.exif_tz}"
|
||||
)
|
||||
if album:
|
||||
rich_echo(
|
||||
echo(
|
||||
f"Compared {len(photos)} photos, found {different_photos} "
|
||||
f"that {pluralize(different_photos, 'is', 'are')} different and "
|
||||
f"added {pluralize(different_photos, 'it', 'them')} to album '{album.name}'."
|
||||
@@ -644,14 +575,14 @@ def timewarp(
|
||||
|
||||
if timezone:
|
||||
tz_updater = PhotoTimeZoneUpdater(
|
||||
timezone, verbose=verbose_, library_path=library
|
||||
timezone, verbose=verbose, library_path=library
|
||||
)
|
||||
|
||||
if any([push_exif, pull_exif, function]):
|
||||
# ExifDateTimeUpdater used to get photo path for --function
|
||||
exif_updater = ExifDateTimeUpdater(
|
||||
library_path=library,
|
||||
verbose=verbose_,
|
||||
verbose=verbose,
|
||||
exiftool_path=exiftool_path,
|
||||
plain=plain,
|
||||
)
|
||||
@@ -662,34 +593,35 @@ def timewarp(
|
||||
f"Processing [num]{num_photos}[/] {pluralize(len(photos), 'photo', 'photos')}",
|
||||
total=num_photos,
|
||||
)
|
||||
for p in photos:
|
||||
for photo in photos:
|
||||
set_crash_data("photo", f"{photo.uuid} {photo.filename}")
|
||||
if parse_date:
|
||||
set_photo_date_from_filename_(photo, photo.filename, parse_date)
|
||||
if pull_exif:
|
||||
exif_updater.update_photos_from_exif(
|
||||
p, use_file_modify_date=use_file_time
|
||||
photo, use_file_modify_date=use_file_time
|
||||
)
|
||||
if any([date, time, date_delta, time_delta]):
|
||||
update_photo_date_time_(p)
|
||||
update_photo_date_time_(photo)
|
||||
if match_time:
|
||||
# need to adjust time before the timezone is updated
|
||||
# or the old timezone will be overwritten in the database
|
||||
update_photo_time_for_new_timezone_(photo=p, new_timezone=timezone)
|
||||
update_photo_time_for_new_timezone_(photo=photo, new_timezone=timezone)
|
||||
if timezone:
|
||||
tz_updater.update_photo(p)
|
||||
tz_updater.update_photo(photo)
|
||||
if function:
|
||||
verbose_(f"Calling function [bold]{function[1]}")
|
||||
photo_path = exif_updater.get_photo_path(p)
|
||||
update_photo_from_function_(photo=p, path=photo_path)
|
||||
verbose(f"Calling function [bold]{function[1]}")
|
||||
photo_path = exif_updater.get_photo_path(photo)
|
||||
update_photo_from_function_(photo=photo, path=photo_path)
|
||||
if push_exif:
|
||||
# this should be the last step in the if chain to ensure all Photos data is updated
|
||||
# before exiftool is run
|
||||
exif_warn, exif_error = exif_updater.update_exif_from_photos(p)
|
||||
exif_warn, exif_error = exif_updater.update_exif_from_photos(photo)
|
||||
if exif_warn:
|
||||
rich_echo_error(
|
||||
f"[warning]Warning running exiftool: {exif_warn}[/]"
|
||||
)
|
||||
echo_error(f"[warning]Warning running exiftool: {exif_warn}[/]")
|
||||
if exif_error:
|
||||
rich_echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
||||
echo_error(f"[error]Error running exiftool: {exif_error}[/]")
|
||||
|
||||
progress.advance(task)
|
||||
|
||||
rich_echo("Done.")
|
||||
echo("Done.")
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
"""helper functions for printing verbose output"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import typing as t
|
||||
from datetime import datetime
|
||||
from typing import IO, Any, Callable, Optional
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
from rich.theme import Theme
|
||||
|
||||
from .click_rich_echo import rich_click_echo
|
||||
from .click_rich_echo import (
|
||||
rich_click_echo,
|
||||
set_rich_console,
|
||||
set_rich_theme,
|
||||
set_rich_timestamp,
|
||||
)
|
||||
from .color_themes import get_theme
|
||||
from .common import CLI_COLOR_ERROR, CLI_COLOR_WARNING, time_stamp
|
||||
|
||||
# set to 1 if running tests
|
||||
@@ -17,14 +25,80 @@ OSXPHOTOS_IS_TESTING = bool(os.getenv("OSXPHOTOS_IS_TESTING", default=False))
|
||||
# include error/warning emoji's in verbose output
|
||||
ERROR_EMOJI = True
|
||||
|
||||
__all__ = ["get_verbose_console", "verbose_print"]
|
||||
# global to store verbose level
|
||||
__verbose_level = 1
|
||||
|
||||
# global verbose function
|
||||
__verbose_function: Callable[..., None] | None = None
|
||||
|
||||
|
||||
__all__ = [
|
||||
"get_verbose_console",
|
||||
"get_verbose_level",
|
||||
"set_verbose_level",
|
||||
"verbose_print",
|
||||
"verbose",
|
||||
]
|
||||
|
||||
|
||||
def _reset_verbose_globals():
|
||||
"""Reset globals for testing"""
|
||||
global __verbose_level
|
||||
global __verbose_function
|
||||
global _console
|
||||
__verbose_level = 1
|
||||
__verbose_function = None
|
||||
_console = _Console()
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
|
||||
|
||||
def verbose(*args, level: int = 1):
|
||||
"""Print verbose output
|
||||
|
||||
Args:
|
||||
*args: arguments to pass to verbose function for printing
|
||||
level: verbose level; if level > get_verbose_level(), output is suppressed
|
||||
"""
|
||||
|
||||
# Notes:
|
||||
# Normally you should use verbose_print() to get the verbose function instead of calling this directly
|
||||
# This is here so that verbose can be directly imported and used in other modules without calling verbose_print()
|
||||
# Use of verbose_print() will set the verbose function so that calling verbose() will work as expected
|
||||
global __verbose_function
|
||||
if __verbose_function is None:
|
||||
return
|
||||
__verbose_function(*args, level=level)
|
||||
|
||||
|
||||
def set_verbose_level(level: int):
|
||||
"""Set verbose level"""
|
||||
global __verbose_level
|
||||
global __verbose_function
|
||||
__verbose_level = level
|
||||
if level > 0 and __verbose_function is None:
|
||||
# if verbose level set but verbose function not set, set it to default
|
||||
# verbose_print sets the global __verbose_function
|
||||
__verbose_function = _verbose_print_function(level)
|
||||
elif level == 0 and __verbose_function is not None:
|
||||
# if verbose level set to 0 but verbose function is set, set it to no-op
|
||||
__verbose_function = noop
|
||||
|
||||
|
||||
def get_verbose_level() -> int:
|
||||
"""Get verbose level"""
|
||||
global __verbose_level
|
||||
return __verbose_level
|
||||
|
||||
|
||||
class _Console:
|
||||
"""Store console object for verbose output"""
|
||||
|
||||
def __init__(self):
|
||||
self._console: t.Optional[Console] = None
|
||||
self._console: Optional[Console] = None
|
||||
|
||||
@property
|
||||
def console(self):
|
||||
@@ -38,12 +112,7 @@ class _Console:
|
||||
_console = _Console()
|
||||
|
||||
|
||||
def noop(*args, **kwargs):
|
||||
"""no-op function"""
|
||||
pass
|
||||
|
||||
|
||||
def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
|
||||
def get_verbose_console(theme: Optional[Theme] = None) -> Console:
|
||||
"""Get console object or create one if not already created
|
||||
|
||||
Args:
|
||||
@@ -59,18 +128,68 @@ def get_verbose_console(theme: t.Optional[Theme] = None) -> Console:
|
||||
|
||||
|
||||
def verbose_print(
|
||||
verbose: int = 1,
|
||||
timestamp: bool = False,
|
||||
rich: bool = True,
|
||||
theme: str | None = None,
|
||||
highlight: bool = False,
|
||||
file: Optional[IO] = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., None]:
|
||||
"""Configure verbose printing and create verbose function to print output
|
||||
|
||||
Args:
|
||||
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
|
||||
timestamp: if True, includes timestamp in verbose output
|
||||
rich: use rich.print instead of click.echo
|
||||
highlight: if True, use automatic rich.print highlighting
|
||||
theme: optional name of theme to use for formatting (will be loaded by get_theme())
|
||||
file: optional file handle to write to instead of stdout
|
||||
kwargs: any extra arguments to pass to click.echo or rich.print depending on whether rich==True
|
||||
|
||||
Returns:
|
||||
function to print output
|
||||
|
||||
Note: sets the console for rich_echo to be the same as the console used for verbose output
|
||||
"""
|
||||
|
||||
set_verbose_level(verbose)
|
||||
color_theme = get_theme(theme)
|
||||
verbose_function = _verbose_print_function(
|
||||
verbose=verbose,
|
||||
timestamp=timestamp,
|
||||
rich=rich,
|
||||
theme=color_theme,
|
||||
highlight=highlight,
|
||||
file=file,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# set console for rich_echo to be same as for verbose
|
||||
set_rich_console(get_verbose_console())
|
||||
set_rich_theme(color_theme)
|
||||
set_rich_timestamp(timestamp)
|
||||
|
||||
# set global verbose function to match
|
||||
global __verbose_function
|
||||
__verbose_function = verbose_function
|
||||
|
||||
return verbose_function
|
||||
|
||||
|
||||
def _verbose_print_function(
|
||||
verbose: bool = True,
|
||||
timestamp: bool = False,
|
||||
rich: bool = False,
|
||||
highlight: bool = False,
|
||||
theme: t.Optional[Theme] = None,
|
||||
file: t.Optional[t.IO] = None,
|
||||
**kwargs: t.Any,
|
||||
) -> t.Callable:
|
||||
theme: Optional[Theme] = None,
|
||||
file: Optional[IO] = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., None]:
|
||||
"""Create verbose function to print output
|
||||
|
||||
Args:
|
||||
verbose: if True, returns verbose print function otherwise returns no-op function
|
||||
verbose: if > 0, returns verbose print function otherwise returns no-op function; the value of verbose is the verbose level
|
||||
timestamp: if True, includes timestamp in verbose output
|
||||
rich: use rich.print instead of click.echo
|
||||
highlight: if True, use automatic rich.print highlighting
|
||||
@@ -81,18 +200,22 @@ def verbose_print(
|
||||
Returns:
|
||||
function to print output
|
||||
"""
|
||||
if not verbose:
|
||||
return noop
|
||||
|
||||
# configure console even if verbose is False so that rich_echo will work correctly
|
||||
global _console
|
||||
if file:
|
||||
_console.console = Console(theme=theme, file=file)
|
||||
else:
|
||||
_console.console = Console(theme=theme, width=10_000)
|
||||
|
||||
if not verbose:
|
||||
return noop
|
||||
|
||||
# closure to capture timestamp
|
||||
def verbose_(*args):
|
||||
def verbose_(*args, level: int = 1):
|
||||
"""print output if verbose flag set"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
styled_args = []
|
||||
timestamp_str = f"{str(datetime.now())} -- " if timestamp else ""
|
||||
for arg in args:
|
||||
@@ -103,10 +226,12 @@ def verbose_print(
|
||||
elif "warning" in arg.lower():
|
||||
arg = click.style(arg, fg=CLI_COLOR_WARNING)
|
||||
styled_args.append(arg)
|
||||
click.echo(*styled_args, **kwargs)
|
||||
click.echo(*styled_args, **kwargs, file=file or None)
|
||||
|
||||
def rich_verbose_(*args):
|
||||
def rich_verbose_(*args, level: int = 1):
|
||||
"""rich.print output if verbose flag set"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
global ERROR_EMOJI
|
||||
timestamp_str = time_stamp() if timestamp else ""
|
||||
new_args = []
|
||||
@@ -124,8 +249,10 @@ def verbose_print(
|
||||
new_args.append(arg)
|
||||
_console.console.print(*new_args, highlight=highlight, **kwargs)
|
||||
|
||||
def rich_verbose_testing_(*args):
|
||||
def rich_verbose_testing_(*args, level: int = 1):
|
||||
"""print output if verbose flag set using rich.print"""
|
||||
if get_verbose_level() < level:
|
||||
return
|
||||
global ERROR_EMOJI
|
||||
timestamp_str = time_stamp() if timestamp else ""
|
||||
new_args = []
|
||||
|
||||
@@ -44,7 +44,7 @@ def crash_reporter(filename, message, title, postamble, *extra_args):
|
||||
f.write(f"Platform: {platform.platform()}\n")
|
||||
f.write(f"Python version: {sys.version}\n")
|
||||
f.write(f"sys.argv: {sys.argv}\n")
|
||||
f.write("CRASH_DATA: \\n")
|
||||
f.write("CRASH_DATA:\n")
|
||||
for k, v in CRASH_DATA.items():
|
||||
f.write(f"{k}: {v}\n")
|
||||
for arg in extra_args:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Utilities for debugging"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
@@ -9,21 +11,33 @@ from typing import Dict, List
|
||||
import wrapt
|
||||
from rich import print
|
||||
|
||||
__all__ = [
|
||||
"debug_breakpoint",
|
||||
"debug_watch",
|
||||
"get_debug_flags",
|
||||
"get_debug_options",
|
||||
"is_debug",
|
||||
"set_debug",
|
||||
"wrap_function",
|
||||
]
|
||||
|
||||
|
||||
# global variable to control debug output
|
||||
# set via --debug
|
||||
DEBUG = False
|
||||
__osxphotos_debug = False
|
||||
|
||||
|
||||
def set_debug(debug: bool):
|
||||
"""set debug flag"""
|
||||
global DEBUG
|
||||
DEBUG = debug
|
||||
global __osxphotos_debug
|
||||
__osxphotos_debug = debug
|
||||
logging.disable(logging.NOTSET if debug else logging.DEBUG)
|
||||
|
||||
|
||||
def is_debug():
|
||||
"""return debug flag"""
|
||||
return DEBUG
|
||||
global __osxphotos_debug
|
||||
return __osxphotos_debug
|
||||
|
||||
|
||||
def debug_watch(wrapped, instance, args, kwargs):
|
||||
@@ -49,7 +63,7 @@ def debug_breakpoint(wrapped, instance, args, kwargs):
|
||||
|
||||
def wrap_function(function_path, wrapper):
|
||||
"""Wrap a function with wrapper function"""
|
||||
module, name = function_path.split(".", 1)
|
||||
module, name = function_path.split("::", 1)
|
||||
try:
|
||||
return wrapt.wrap_function_wrapper(module, name, wrapper)
|
||||
except AttributeError as e:
|
||||
|
||||
Binary file not shown.
@@ -41,6 +41,11 @@ ExifDateTime = namedtuple(
|
||||
|
||||
def exif_offset_to_seconds(offset: str) -> int:
|
||||
"""Convert timezone offset from UTC in exiftool format (+/-hh:mm) to seconds"""
|
||||
|
||||
# Z (for Zulu time) corresponds to a zero UTC offset
|
||||
if offset == "Z":
|
||||
return 0
|
||||
|
||||
sign = 1 if offset[0] == "+" else -1
|
||||
hours, minutes = offset[1:].split(":")
|
||||
return sign * (int(hours) * 3600 + int(minutes) * 60)
|
||||
@@ -280,6 +285,7 @@ def get_exif_date_time_offset(
|
||||
time_fields = [
|
||||
"EXIF:DateTimeOriginal",
|
||||
"EXIF:CreateDate",
|
||||
"QuickTime:ContentCreateDate",
|
||||
"QuickTime:CreationDate",
|
||||
"QuickTime:CreateDate",
|
||||
"IPTC:DateCreated",
|
||||
|
||||
@@ -123,6 +123,7 @@ class _ExifToolProc:
|
||||
)
|
||||
return
|
||||
self._process_running = False
|
||||
self._large_file_support = large_file_support
|
||||
self._exiftool = exiftool or get_exiftool_path()
|
||||
self._start_proc(large_file_support=large_file_support)
|
||||
|
||||
@@ -130,7 +131,7 @@ class _ExifToolProc:
|
||||
def process(self):
|
||||
"""return the exiftool subprocess"""
|
||||
if not self._process_running:
|
||||
self._start_proc()
|
||||
self._start_proc(large_file_support=self._large_file_support)
|
||||
return self._process
|
||||
|
||||
@property
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user