Compare commits
88 Commits
v0.10.1-al
...
v0.14.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7818fe0fcf | ||
|
|
792fff13b8 | ||
|
|
b2242da9b7 | ||
|
|
1e5633efc8 | ||
|
|
27b3469513 | ||
|
|
aa25c9eab7 | ||
|
|
44321da243 | ||
|
|
67127ef370 | ||
|
|
51e720dce9 | ||
|
|
45b8150d5f | ||
|
|
6ebacb7f38 | ||
|
|
1b17608a02 | ||
|
|
d4353d48bd | ||
|
|
5af2b3e039 | ||
|
|
b36b7e7eb2 | ||
|
|
99be93d88f | ||
|
|
d840c11ddb | ||
|
|
b1fd019120 | ||
|
|
8b4a386c13 | ||
|
|
6abd10eddf | ||
|
|
aa73c2f055 | ||
|
|
966954340b | ||
|
|
7732708cb2 | ||
|
|
a7d9f72372 | ||
|
|
eabda3ea93 | ||
|
|
a995a8d610 | ||
|
|
ecd75c775d | ||
|
|
ae7fc69b33 | ||
|
|
83186a655d | ||
|
|
243492df88 | ||
|
|
986a092130 | ||
|
|
1c323338c6 | ||
|
|
b005f70133 | ||
|
|
9023a69073 | ||
|
|
e7958c94e8 | ||
|
|
17bc04a24a | ||
|
|
e0b1113870 | ||
|
|
10f0cf1092 | ||
|
|
a4b5f2a501 | ||
|
|
02fcfbca2c | ||
|
|
7eff015439 | ||
|
|
1c7e81b578 | ||
|
|
8649cda11f | ||
|
|
0c152c8c91 | ||
|
|
b0a9e87d00 | ||
|
|
fc6d7b1cf5 | ||
|
|
5234567974 | ||
|
|
f62a9d3d4e | ||
|
|
d311431c91 | ||
|
|
572e32b71b | ||
|
|
3744a596b5 | ||
|
|
221df0029e | ||
|
|
a1038314e2 | ||
|
|
39ef8ddf3f | ||
|
|
292f3e5ea4 | ||
|
|
e9a4de447c | ||
|
|
42bc929142 | ||
|
|
58f52833d6 | ||
|
|
7cf8dcf22a | ||
|
|
e139fbb2fb | ||
|
|
332cbe3e37 | ||
|
|
e35876a1ba | ||
|
|
693d27c049 | ||
|
|
8742967885 | ||
|
|
a27cf45f95 | ||
|
|
93cc305fa7 | ||
|
|
0d51798384 | ||
|
|
22e466cd0e | ||
|
|
33ef56fd94 | ||
|
|
6b24706851 | ||
|
|
be3d6dee8d | ||
|
|
07c9c31a09 | ||
|
|
9db32a51ca | ||
|
|
5200cd69ce | ||
|
|
0e65ab5452 | ||
|
|
cbdc193b83 | ||
|
|
fb2c12d981 | ||
|
|
81be373505 | ||
|
|
5f1bd5f13d | ||
|
|
7ef472fcb2 | ||
|
|
a58ac149f3 | ||
|
|
32bd4cfb9d | ||
|
|
9b44adff34 | ||
|
|
75c25c8bf5 | ||
|
|
a79472ba0c | ||
|
|
cafb88173f | ||
|
|
af122e9392 | ||
|
|
470585bf8c |
34
.github/workflows/pythonpackage.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Python package
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macOS-10.14
|
||||
strategy:
|
||||
max-parallel: 4
|
||||
matrix:
|
||||
python-version: [3.6, 3.7]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# pip install flake8
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install pytest
|
||||
python -m pytest tests/
|
||||
135
README.md
@@ -1,12 +1,22 @@
|
||||
# [OSXPhotos](https://github.com/RhetTbull/osxphotos)
|
||||
# OSXPhotos
|
||||
|
||||
[](https://github.com/python/black)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
|
||||
## What is osxphotos?
|
||||
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos app library database on Mac OS X.
|
||||
OSXPhotos provides the ability to interact with and query Apple's Photos app library database on MacOS. Using this module you can query the Photos database for information about the photos stored in a Photos library--for example, file name, file path, and metadata such as keywords/tags, persons/faces, albums, etc.
|
||||
|
||||
NOTE: OSXPhotos currently only supports image files -- e.g. it does not handle movies.
|
||||
|
||||
## Supported operating systems
|
||||
|
||||
Only works on Mac OS X. Only tested on Mac OS 10.13 and Photos 3.0. Requires python >= 3.6
|
||||
Only works on MacOS (aka Mac OS X). Tested on MacOS 10.12.6 / Photos 2.0, 10.13.6 / Photos 3.0 and MacOS 10.14.5, 10.14.6 / Photos 4.0. Requires python >= 3.6
|
||||
|
||||
NOTE: Alpha support for Mac OS 10.15.0 / Photos 5.0. Photos 5.0 uses a new database format which required rewrite of much of the code for this module. If you find bugs, please open an [issue](https://github.com/RhetTbull/osxphotos/issues/).
|
||||
|
||||
This module will read Photos databases for any supported version on any supported OS version. E.g. you can read a database created with Photos 4.0 on MacOS 10.14 on a machine running MacOS 10.12
|
||||
|
||||
|
||||
## Installation instructions
|
||||
|
||||
@@ -16,9 +26,69 @@ osxmetadata uses setuptools, thus simply run:
|
||||
|
||||
## Command Line Usage
|
||||
|
||||
This project started as a command line utility, `photosmeta`, available at [photosmeta](https://github.com/RhetTbull/photosmeta) This module converts the photosmeta functionality into a module. Eventually, I plan to rewrite photosmeta using this module and include it as a command line script in this module.
|
||||
This module will install a command line utility called `osxphotos` that allows you to query the Photos database.
|
||||
|
||||
## Example uses of the module
|
||||
If you only care about the command line tool, I recommend installing with [pipx](https://github.com/pipxproject/pipx)
|
||||
|
||||
After installing pipx:
|
||||
`pipx install osxphotos`
|
||||
|
||||
```
|
||||
Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
|
||||
|
||||
Options:
|
||||
--db <Photos database path> Specify database file
|
||||
--json Print output in JSON format
|
||||
-h, --help Show this message and exit.
|
||||
|
||||
Commands:
|
||||
albums print out albums found in the Photos library
|
||||
dump print list of all photos & associated info from the Photos...
|
||||
help print help; for help on commands: help <command>
|
||||
info print out descriptive info of the Photos library database
|
||||
keywords print out keywords found in the Photos library
|
||||
persons print out persons (faces) found in the Photos library
|
||||
query query the Photos database using 1 or more search options
|
||||
```
|
||||
|
||||
To get help on a specific command, use `osxphotos help <command_name>`
|
||||
|
||||
Example: `osxphotos help query`
|
||||
|
||||
```
|
||||
Usage: osxphotos help [OPTIONS]
|
||||
|
||||
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)
|
||||
|
||||
Options:
|
||||
--keyword TEXT search for keyword(s)
|
||||
--person TEXT search for person(s)
|
||||
--album TEXT search for album(s)
|
||||
--uuid TEXT search for UUID(s)
|
||||
--name TEXT search for TEXT in name of photo
|
||||
--no-name search for photos with no name
|
||||
--description TEXT search for TEXT in description of photo
|
||||
--no-description search for photos with no description
|
||||
-i, --ignore-case case insensitive search for name or description. Does
|
||||
not apply to keyword, person, or album
|
||||
--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
|
||||
--missing search for photos missing from disk
|
||||
--not-missing search for photos present on disk (e.g. not missing)
|
||||
--json Print output in JSON format
|
||||
-h, --help Show this message and exit.
|
||||
```
|
||||
|
||||
Example: find all photos with keyword "Kids" and output results to json file named results.json:
|
||||
|
||||
`osxphotos query --keyword Kids --json >results.json`
|
||||
|
||||
## Example uses of the module
|
||||
|
||||
```python
|
||||
import osxphotos
|
||||
@@ -43,6 +113,7 @@ def main():
|
||||
print(
|
||||
p.uuid,
|
||||
p.filename(),
|
||||
p.original_filename(),
|
||||
p.date(),
|
||||
p.description(),
|
||||
p.name(),
|
||||
@@ -143,6 +214,23 @@ photosdb.get_photos_library_path()
|
||||
|
||||
Returns the path to the Photos library as a string
|
||||
|
||||
#### ```get_db_path```
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photosdb.get_db_path()
|
||||
```
|
||||
|
||||
Returns the path to the Photos database PhotosDB was initialized with
|
||||
|
||||
#### ```get_db_version```
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
photosdb.get_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```
|
||||
```python
|
||||
# assumes photosdb is a PhotosDB object (see above)
|
||||
@@ -216,7 +304,10 @@ PhotosDB.photos() returns a list of PhotoInfo objects. Each PhotoInfo object re
|
||||
Returns the universally unique identifier (uuid) of the photo. This is how Photos keeps track of individual photos within the database.
|
||||
|
||||
#### `filename()`
|
||||
Returns the filename of the photo
|
||||
Returns the current filename of the photo on disk. See also `original_filename()`
|
||||
|
||||
#### `original_filename()`
|
||||
Returns the original filename of the photo when it was imported to Photos. Note: Photos 5.0+ renames the photo when it adds the file to the library using UUID. See also `filename()`
|
||||
|
||||
#### `date()`
|
||||
Returns the date of the photo as a datetime.datetime object
|
||||
@@ -237,7 +328,7 @@ Returns a list of albums the photo is contained in
|
||||
Returns a list of the names of the persons in the photo
|
||||
|
||||
#### `path()`
|
||||
Returns the path to the photo on disk as a string. Note: this returns the path to the *original* unedited file (see `hasadjustments()`). If the file is missing on disk, path=`None` (see `ismissing()`)
|
||||
Returns the absolute path to the photo on disk as a string. Note: this returns the path to the *original* unedited file (see `hasadjustments()`). If the file is missing on disk, path=`None` (see `ismissing()`)
|
||||
|
||||
#### `ismissing()`
|
||||
Returns `True` if the original image file is missing on disk, otherwise `False`. This can occur if the file has been uploaded to iCloud but not yet downloaded to the local library or if the file was deleted or imported from a disk that has been unmounted. Note: this status is set by Photos and osxphotos does not verify that the file path returned by `path()` actually exists. It merely reports what Photos has stored in the library database.
|
||||
@@ -245,6 +336,18 @@ Returns `True` if the original image file is missing on disk, otherwise `False`.
|
||||
#### `hasadjustments()`
|
||||
Returns `True` if the file has been edited in Photos, otherwise `False`
|
||||
|
||||
#### `favorite()`
|
||||
Returns `True` if the picture has been marked as a favorite, otherwise `False`
|
||||
|
||||
#### `hidden()`
|
||||
Returns `True` if the picture has been marked as hidden, otherwise `False`
|
||||
|
||||
#### `location()`
|
||||
Returns latitude and longitude as a tuple of floats (latitude, longitude). If location is not set, latitude and longitude are returned as `None`
|
||||
|
||||
#### `to_json()`
|
||||
Returns a JSON representation of all photo info
|
||||
|
||||
Examples:
|
||||
|
||||
```python
|
||||
@@ -254,6 +357,7 @@ for p in photos:
|
||||
print(
|
||||
p.uuid(),
|
||||
p.filename(),
|
||||
p.original_filename(),
|
||||
p.date(),
|
||||
p.description(),
|
||||
p.name(),
|
||||
@@ -266,19 +370,26 @@ for p in photos:
|
||||
)
|
||||
```
|
||||
|
||||
## History
|
||||
|
||||
This project started as a command line utility, `photosmeta`, available at [photosmeta](https://github.com/RhetTbull/photosmeta) This module converts the photosmeta Photos library query functionality into a module.
|
||||
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
This module is very kludgy. It works by creating a copy of the sqlite3 database that Photos uses to store data about the Photos library. The class PhotosDB then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc.
|
||||
This module works by creating a copy of the sqlite3 database that Photos uses to store data about the Photos library. The class PhotosDB then queries this database to extract information about the photos such as persons (faces identified in the photos), albums, keywords, etc.
|
||||
|
||||
If Apple changes the database format this will likely break.
|
||||
If Apple changes the database format this will likely break.
|
||||
|
||||
The sqlite3 database used by Photos uses write ahead logging that is updated asynchronously in the background by a Photos helper service. Sometimes the update takes a long time meaning the latest changes made in Photos (e.g. add a keyword) will not show up in the database for sometime. I know of no way around this.
|
||||
Apple does provide an framework ([PhotoKit](https://developer.apple.com/documentation/photokit?language=objc)) for querying the user's Photos library and I attempted to create the funcationality in this module using this framework but unfortunately PhotoKit does not provide access to much of the needed metadata (such as Faces/Persons). While copying the sqlite file is a bit kludgy, it allows osxphotos to provide access to all available metadata.
|
||||
|
||||
## Dependencies
|
||||
[PyObjC](https://pythonhosted.org/pyobjc/)
|
||||
- [PyObjC](https://pythonhosted.org/pyobjc/)
|
||||
- [PyYAML](https://pypi.org/project/PyYAML/)
|
||||
- [Click](https://pypi.org/project/click/)
|
||||
|
||||
## Acknowledgements
|
||||
This code was inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se
|
||||
This project was inspired by photo-export by Patrick Fältström see: (https://github.com/patrikhson/photo-export) Copyright (c) 2015 Patrik Fältström paf@frobbit.se
|
||||
|
||||
To interact with the Photos app, I use [py-applescript]( https://github.com/rdhyee/py-applescript) by "Raymond Yee / rdhyee". Rather than import this module, I included the entire module
|
||||
(which is published as public domain code) in a private module to prevent ambiguity with
|
||||
|
||||
@@ -3,6 +3,9 @@ import osxphotos
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
print(f"db file = {photosdb.get_db_path()}")
|
||||
print(f"db version = {photosdb.get_db_version()}")
|
||||
|
||||
print(photosdb.keywords())
|
||||
print(photosdb.persons())
|
||||
print(photosdb.albums())
|
||||
|
||||
358
osxphotos/cmd_line.py
Normal file
@@ -0,0 +1,358 @@
|
||||
import csv
|
||||
import json
|
||||
import sys
|
||||
|
||||
import click
|
||||
import yaml
|
||||
|
||||
import osxphotos
|
||||
|
||||
# TODO: add query for description, name (contains text)
|
||||
# TODO: add "--any" to search any field (e.g. keyword, description, name contains "wedding") (add case insensitive option)
|
||||
|
||||
|
||||
class CLI_Obj:
|
||||
def __init__(self, db=None, json=False):
|
||||
self.photosdb = osxphotos.PhotosDB(dbfile=db)
|
||||
self.json = json
|
||||
|
||||
|
||||
CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
|
||||
|
||||
@click.group(context_settings=CTX_SETTINGS)
|
||||
@click.option(
|
||||
"--db",
|
||||
required=False,
|
||||
metavar="<Photos database path>",
|
||||
default=None,
|
||||
help="Specify database file",
|
||||
)
|
||||
@click.option(
|
||||
"--json",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format",
|
||||
)
|
||||
@click.pass_context
|
||||
def cli(ctx, db, json):
|
||||
ctx.obj = CLI_Obj(db=db, json=json)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def keywords(cli_obj):
|
||||
""" print out keywords found in the Photos library"""
|
||||
keywords = {"keywords": cli_obj.photosdb.keywords_as_dict()}
|
||||
if cli_obj.json:
|
||||
print(json.dumps(keywords))
|
||||
else:
|
||||
print(yaml.dump(keywords, sort_keys=False))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def albums(cli_obj):
|
||||
""" print out albums found in the Photos library """
|
||||
albums = {"albums": cli_obj.photosdb.albums_as_dict()}
|
||||
if cli_obj.json:
|
||||
print(json.dumps(albums))
|
||||
else:
|
||||
print(yaml.dump(albums, sort_keys=False))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def persons(cli_obj):
|
||||
""" print out persons (faces) found in the Photos library """
|
||||
persons = {"persons": cli_obj.photosdb.persons_as_dict()}
|
||||
if cli_obj.json:
|
||||
print(json.dumps(persons))
|
||||
else:
|
||||
print(yaml.dump(persons, sort_keys=False))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def info(cli_obj):
|
||||
""" print out descriptive info of the Photos library database """
|
||||
pdb = cli_obj.photosdb
|
||||
info = {}
|
||||
info["database_path"] = pdb.get_db_path()
|
||||
info["database_version"] = pdb.get_db_version()
|
||||
|
||||
photos = pdb.photos()
|
||||
info["photo_count"] = len(photos)
|
||||
|
||||
keywords = pdb.keywords_as_dict()
|
||||
info["keywords_count"] = len(keywords)
|
||||
info["keywords"] = keywords
|
||||
|
||||
albums = pdb.albums_as_dict()
|
||||
info["albums_count"] = len(albums)
|
||||
info["albums"] = albums
|
||||
|
||||
persons = pdb.persons_as_dict()
|
||||
# handle empty person names (added by Photos 5.0+ when face detected but not identified)
|
||||
noperson = "UNKNOWN"
|
||||
if "" in persons:
|
||||
if noperson in persons:
|
||||
persons[noperson].append(persons[""])
|
||||
else:
|
||||
persons[noperson] = persons[""]
|
||||
persons.pop("", None)
|
||||
|
||||
info["persons_count"] = len(persons)
|
||||
info["persons"] = persons
|
||||
|
||||
if cli_obj.json:
|
||||
print(json.dumps(info))
|
||||
else:
|
||||
print(yaml.dump(info, sort_keys=False))
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.pass_obj
|
||||
def dump(cli_obj):
|
||||
""" print list of all photos & associated info from the Photos library """
|
||||
pdb = cli_obj.photosdb
|
||||
photos = pdb.photos()
|
||||
print_photo_info(photos, cli_obj.json)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.option("--keyword", default=None, multiple=True, help="search for keyword(s)")
|
||||
@click.option("--person", default=None, multiple=True, help="search for person(s)")
|
||||
@click.option("--album", default=None, multiple=True, help="search for album(s)")
|
||||
@click.option("--uuid", default=None, multiple=True, help="search for UUID(s)")
|
||||
@click.option(
|
||||
"--name", default=None, multiple=True, help="search for TEXT in name of photo"
|
||||
)
|
||||
@click.option("--no-name", is_flag=True, help="search for photos with no name")
|
||||
@click.option(
|
||||
"--description",
|
||||
default=None,
|
||||
multiple=True,
|
||||
help="search for TEXT in description of photo",
|
||||
)
|
||||
@click.option("--no-description", is_flag=True, help="search for photos with no description")
|
||||
@click.option(
|
||||
"-i",
|
||||
"--ignore-case",
|
||||
is_flag=True,
|
||||
help="case insensitive search for name or description. Does not apply to keyword, person, or album",
|
||||
)
|
||||
@click.option("--favorite", is_flag=True, help="search for photos marked favorite")
|
||||
@click.option(
|
||||
"--not-favorite", is_flag=True, help="search for photos not marked favorite"
|
||||
)
|
||||
@click.option("--hidden", is_flag=True, help="search for photos marked hidden")
|
||||
@click.option("--not-hidden", is_flag=True, help="search for photos not marked hidden")
|
||||
@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(
|
||||
"--json",
|
||||
required=False,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format",
|
||||
)
|
||||
@click.pass_obj
|
||||
@click.pass_context
|
||||
def query(
|
||||
ctx,
|
||||
cli_obj,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
uuid,
|
||||
name,
|
||||
no_name,
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
json,
|
||||
favorite,
|
||||
not_favorite,
|
||||
hidden,
|
||||
not_hidden,
|
||||
missing,
|
||||
not_missing,
|
||||
):
|
||||
""" Query the Photos database using 1 or more search options\n
|
||||
If more than one option is provided, they are treated as "AND"
|
||||
(e.g. search for photos matching all options)
|
||||
"""
|
||||
|
||||
# if no query terms, show help and return
|
||||
if (
|
||||
not keyword
|
||||
and not person
|
||||
and not album
|
||||
and not uuid
|
||||
and not name
|
||||
and not no_name
|
||||
and not description
|
||||
and not no_description
|
||||
and not favorite
|
||||
and not not_favorite
|
||||
and not hidden
|
||||
and not not_hidden
|
||||
and not missing
|
||||
and not not_missing
|
||||
):
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif favorite and not_favorite:
|
||||
# can't search for both favorite and notfavorite
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif hidden and not_hidden:
|
||||
# can't search for both hidden and nothidden
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif missing and not_missing:
|
||||
# can't search for both missing and notmissing
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif name and no_name:
|
||||
# can't search for both name and no_name
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
elif description and no_description:
|
||||
# can't search for both description and no_description
|
||||
print(cli.commands["query"].get_help(ctx))
|
||||
return
|
||||
else:
|
||||
photos = cli_obj.photosdb.photos(
|
||||
keywords=keyword, persons=person, albums=album, uuid=uuid
|
||||
)
|
||||
|
||||
if name:
|
||||
# search name field for text
|
||||
# if more than one, find photos with all name values in in name
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for n in name:
|
||||
n = n.lower()
|
||||
photos = [p for p in photos if p.name() and n in p.name().lower()]
|
||||
else:
|
||||
for n in name:
|
||||
photos = [p for p in photos if p.name() and n in p.name()]
|
||||
elif no_name:
|
||||
photos = [p for p in photos if not p.name()]
|
||||
|
||||
if description:
|
||||
# search description field for text
|
||||
# if more than one, find photos with all name values in in description
|
||||
if ignore_case:
|
||||
# case-insensitive
|
||||
for d in description:
|
||||
d = d.lower()
|
||||
photos = [
|
||||
p
|
||||
for p in photos
|
||||
if p.description() and d in p.description().lower()
|
||||
]
|
||||
else:
|
||||
for d in description:
|
||||
photos = [
|
||||
p for p in photos if p.description() and d in p.description()
|
||||
]
|
||||
elif no_description:
|
||||
photos = [p for p in photos if not p.description()]
|
||||
|
||||
if favorite:
|
||||
photos = [p for p in photos if p.favorite()]
|
||||
elif not_favorite:
|
||||
photos = [p for p in photos if not p.favorite()]
|
||||
|
||||
if hidden:
|
||||
photos = [p for p in photos if p.hidden()]
|
||||
elif not_hidden:
|
||||
photos = [p for p in photos if not p.hidden()]
|
||||
|
||||
if missing:
|
||||
photos = [p for p in photos if p.ismissing()]
|
||||
elif not_missing:
|
||||
photos = [p for p in photos if not p.ismissing()]
|
||||
|
||||
print_photo_info(photos, cli_obj.json or json)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@click.argument("topic", default=None, required=False, nargs=1)
|
||||
@click.pass_context
|
||||
def help(ctx, topic, **kw):
|
||||
""" print help; for help on commands: help <command> """
|
||||
if topic is None:
|
||||
print(ctx.parent.get_help())
|
||||
else:
|
||||
print(cli.commands[topic].get_help(ctx))
|
||||
|
||||
|
||||
def print_photo_info(photos, json=False):
|
||||
if json:
|
||||
dump = []
|
||||
for p in photos:
|
||||
dump.append(p.to_json())
|
||||
print(f"[{', '.join(dump)}]")
|
||||
else:
|
||||
# dump as CSV
|
||||
csv_writer = csv.writer(
|
||||
sys.stdout, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL
|
||||
)
|
||||
dump = []
|
||||
# add headers
|
||||
dump.append(
|
||||
[
|
||||
"uuid",
|
||||
"filename",
|
||||
"original_filename",
|
||||
"date",
|
||||
"description",
|
||||
"name",
|
||||
"keywords",
|
||||
"albums",
|
||||
"persons",
|
||||
"path",
|
||||
"ismissing",
|
||||
"hasadjustments",
|
||||
"favorite",
|
||||
"hidden",
|
||||
"latitude",
|
||||
"longitude",
|
||||
]
|
||||
)
|
||||
for p in photos:
|
||||
dump.append(
|
||||
[
|
||||
p.uuid(),
|
||||
p.filename(),
|
||||
p.original_filename(),
|
||||
str(p.date()),
|
||||
p.description(),
|
||||
p.name(),
|
||||
", ".join(p.keywords()),
|
||||
", ".join(p.albums()),
|
||||
", ".join(p.persons()),
|
||||
p.path(),
|
||||
p.ismissing(),
|
||||
p.hasadjustments(),
|
||||
p.favorite(),
|
||||
p.hidden(),
|
||||
p._latitude(),
|
||||
p._longitude(),
|
||||
]
|
||||
)
|
||||
for row in dump:
|
||||
csv_writer.writerow(row)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli()
|
||||
126
requirements.txt
@@ -1,4 +1,122 @@
|
||||
# pip install -r requirements.txt
|
||||
|
||||
pyobjc-core
|
||||
pyobjc
|
||||
ansimarkup==1.4.0
|
||||
astroid==2.2.5
|
||||
atomicwrites==1.3.0
|
||||
attrs==19.1.0
|
||||
better-exceptions-fork==0.2.1.post6
|
||||
certifi==2019.3.9
|
||||
Click==7.0
|
||||
colorama==0.4.1
|
||||
importlib-metadata==0.18
|
||||
isort==4.3.20
|
||||
lazy-object-proxy==1.4.1
|
||||
mccabe==0.6.1
|
||||
more-itertools==7.2.0
|
||||
packaging==19.0
|
||||
pluggy==0.12.0
|
||||
py==1.8.0
|
||||
Pygments==2.4.2
|
||||
pylint==2.3.1
|
||||
pyobjc==5.2
|
||||
pyobjc-core==5.2
|
||||
pyobjc-framework-Accounts==5.2
|
||||
pyobjc-framework-AddressBook==5.2
|
||||
pyobjc-framework-AdSupport==5.2
|
||||
pyobjc-framework-AppleScriptKit==5.2
|
||||
pyobjc-framework-AppleScriptObjC==5.2
|
||||
pyobjc-framework-ApplicationServices==5.2
|
||||
pyobjc-framework-Automator==5.2
|
||||
pyobjc-framework-AVFoundation==5.2
|
||||
pyobjc-framework-AVKit==5.2
|
||||
pyobjc-framework-BusinessChat==5.2
|
||||
pyobjc-framework-CalendarStore==5.2
|
||||
pyobjc-framework-CFNetwork==5.2
|
||||
pyobjc-framework-CloudKit==5.2
|
||||
pyobjc-framework-Cocoa==5.2
|
||||
pyobjc-framework-Collaboration==5.2
|
||||
pyobjc-framework-ColorSync==5.2
|
||||
pyobjc-framework-Contacts==5.2
|
||||
pyobjc-framework-ContactsUI==5.2
|
||||
pyobjc-framework-CoreAudio==5.2
|
||||
pyobjc-framework-CoreAudioKit==5.2
|
||||
pyobjc-framework-CoreBluetooth==5.2
|
||||
pyobjc-framework-CoreData==5.2
|
||||
pyobjc-framework-CoreLocation==5.2
|
||||
pyobjc-framework-CoreMedia==5.2
|
||||
pyobjc-framework-CoreMediaIO==5.2
|
||||
pyobjc-framework-CoreML==5.2
|
||||
pyobjc-framework-CoreServices==5.2
|
||||
pyobjc-framework-CoreSpotlight==5.2
|
||||
pyobjc-framework-CoreText==5.2
|
||||
pyobjc-framework-CoreWLAN==5.2
|
||||
pyobjc-framework-CryptoTokenKit==5.2
|
||||
pyobjc-framework-DictionaryServices==5.2
|
||||
pyobjc-framework-DiscRecording==5.2
|
||||
pyobjc-framework-DiscRecordingUI==5.2
|
||||
pyobjc-framework-DiskArbitration==5.2
|
||||
pyobjc-framework-DVDPlayback==5.2
|
||||
pyobjc-framework-EventKit==5.2
|
||||
pyobjc-framework-ExceptionHandling==5.2
|
||||
pyobjc-framework-ExternalAccessory==5.2
|
||||
pyobjc-framework-FinderSync==5.2
|
||||
pyobjc-framework-FSEvents==5.2
|
||||
pyobjc-framework-GameCenter==5.2
|
||||
pyobjc-framework-GameController==5.2
|
||||
pyobjc-framework-GameKit==5.2
|
||||
pyobjc-framework-GameplayKit==5.2
|
||||
pyobjc-framework-ImageCaptureCore==5.2
|
||||
pyobjc-framework-IMServicePlugIn==5.2
|
||||
pyobjc-framework-InputMethodKit==5.2
|
||||
pyobjc-framework-InstallerPlugins==5.2
|
||||
pyobjc-framework-InstantMessage==5.2
|
||||
pyobjc-framework-Intents==5.2
|
||||
pyobjc-framework-IOSurface==5.2
|
||||
pyobjc-framework-iTunesLibrary==5.2
|
||||
pyobjc-framework-LatentSemanticMapping==5.2
|
||||
pyobjc-framework-LaunchServices==5.2
|
||||
pyobjc-framework-libdispatch==5.2
|
||||
pyobjc-framework-LocalAuthentication==5.2
|
||||
pyobjc-framework-MapKit==5.2
|
||||
pyobjc-framework-MediaAccessibility==5.2
|
||||
pyobjc-framework-MediaLibrary==5.2
|
||||
pyobjc-framework-MediaPlayer==5.2
|
||||
pyobjc-framework-MediaToolbox==5.2
|
||||
pyobjc-framework-ModelIO==5.2
|
||||
pyobjc-framework-MultipeerConnectivity==5.2
|
||||
pyobjc-framework-NaturalLanguage==5.2
|
||||
pyobjc-framework-NetFS==5.2
|
||||
pyobjc-framework-Network==5.2
|
||||
pyobjc-framework-NetworkExtension==5.2
|
||||
pyobjc-framework-NotificationCenter==5.2
|
||||
pyobjc-framework-OpenDirectory==5.2
|
||||
pyobjc-framework-OSAKit==5.2
|
||||
pyobjc-framework-Photos==5.2
|
||||
pyobjc-framework-PhotosUI==5.2
|
||||
pyobjc-framework-PreferencePanes==5.2
|
||||
pyobjc-framework-PubSub==5.2
|
||||
pyobjc-framework-QTKit==5.2
|
||||
pyobjc-framework-Quartz==5.2
|
||||
pyobjc-framework-SafariServices==5.2
|
||||
pyobjc-framework-SceneKit==5.2
|
||||
pyobjc-framework-ScreenSaver==5.2
|
||||
pyobjc-framework-ScriptingBridge==5.2
|
||||
pyobjc-framework-SearchKit==5.2
|
||||
pyobjc-framework-Security==5.2
|
||||
pyobjc-framework-SecurityFoundation==5.2
|
||||
pyobjc-framework-SecurityInterface==5.2
|
||||
pyobjc-framework-ServiceManagement==5.2
|
||||
pyobjc-framework-Social==5.2
|
||||
pyobjc-framework-SpriteKit==5.2
|
||||
pyobjc-framework-StoreKit==5.2
|
||||
pyobjc-framework-SyncServices==5.2
|
||||
pyobjc-framework-SystemConfiguration==5.2
|
||||
pyobjc-framework-UserNotifications==5.2
|
||||
pyobjc-framework-VideoSubscriberAccount==5.2
|
||||
pyobjc-framework-VideoToolbox==5.2
|
||||
pyobjc-framework-Vision==5.2
|
||||
pyobjc-framework-WebKit==5.2
|
||||
pyparsing==2.4.1.1
|
||||
PyYAML==5.1.2
|
||||
six==1.12.0
|
||||
wcwidth==0.1.7
|
||||
wrapt==1.11.1
|
||||
zipp==0.5.2
|
||||
|
||||
10
setup.py
@@ -38,7 +38,7 @@ with open(path.join(this_directory, "README.md"), encoding="utf-8") as f:
|
||||
|
||||
setup(
|
||||
name="osxphotos",
|
||||
version="0.10.1",
|
||||
version="0.14.6",
|
||||
description="Manipulate (read-only) Apple's Photos app library on Mac OS X",
|
||||
long_description=long_description,
|
||||
long_description_content_type="text/markdown",
|
||||
@@ -58,8 +58,8 @@ setup(
|
||||
"Programming Language :: Python :: 3.6",
|
||||
"Topic :: Software Development :: Libraries :: Python Modules",
|
||||
],
|
||||
install_requires=["pyobjc",],
|
||||
# entry_points = {
|
||||
# 'console_scripts' : ['osxmetadata=osxmetadata.cmd_line:main'],
|
||||
# }
|
||||
install_requires=["pyobjc","Click","pyyaml",],
|
||||
entry_points = {
|
||||
'console_scripts' : ['osxphotos=osxphotos.cmd_line:cli'],
|
||||
}
|
||||
)
|
||||
|
||||
|
After Width: | Height: | Size: 541 KiB |
|
After Width: | Height: | Size: 528 KiB |
|
After Width: | Height: | Size: 574 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 500 KiB |
|
After Width: | Height: | Size: 450 KiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>DatabaseMinorVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>DatabaseVersion</key>
|
||||
<integer>112</integer>
|
||||
<key>LastOpenMode</key>
|
||||
<integer>2</integer>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
<integer>2622</integer>
|
||||
<key>MetaSchemaVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>createDate</key>
|
||||
<date>2019-08-24T02:50:48Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
tests/Test-10.12.6.photoslibrary/database/RKAlbum_name.skindex
Normal file
BIN
tests/Test-10.12.6.photoslibrary/database/RKMemory_title.skindex
Normal file
BIN
tests/Test-10.12.6.photoslibrary/database/metaSchema.db
Normal file
BIN
tests/Test-10.12.6.photoslibrary/database/photos.db
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LithiumMessageTracer</key>
|
||||
<dict>
|
||||
<key>LastReportedDate</key>
|
||||
<date>2019-08-24T02:50:48Z</date>
|
||||
</dict>
|
||||
<key>PXPeopleScreenUnlocked</key>
|
||||
<true/>
|
||||
<key>Photos</key>
|
||||
<dict>
|
||||
<key>IPXWorkspaceControllerPhotosHasContentKey</key>
|
||||
<true/>
|
||||
<key>IPXWorkspaceControllerZoomLevelsKey</key>
|
||||
<dict>
|
||||
<key>kZoomLevelIdentifierAlbums</key>
|
||||
<integer>7</integer>
|
||||
<key>kZoomLevelIdentifierVersions</key>
|
||||
<integer>7</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2019-08-24T02:51:33Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2019-08-24T13:19:30Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 460 KiB |
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PLLanguageAndLocaleKey</key>
|
||||
<string>en-US:en_US</string>
|
||||
<key>PLLastGeoProviderIdKey</key>
|
||||
<string>7618</string>
|
||||
<key>PLLastLocationInfoFormatVer</key>
|
||||
<integer>12</integer>
|
||||
<key>PLLastRevGeoForcedProviderOutOfDateCheckVersionKey</key>
|
||||
<integer>1</integer>
|
||||
<key>PLLastRevGeoVerFileFetchDateKey</key>
|
||||
<date>2019-08-24T02:51:30Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>414</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>E3E46F2A-7168-4973-AB3E-5848F80BFC7D</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
<integer>2622</integer>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>FileVersion</key>
|
||||
<integer>11</integer>
|
||||
<key>Source</key>
|
||||
<dict>
|
||||
<key>35230</key>
|
||||
<dict>
|
||||
<key>CountryMinVersions</key>
|
||||
<dict>
|
||||
<key>OTHER</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>CurrentVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>NoResultErrorIsSuccess</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>57879</key>
|
||||
<dict>
|
||||
<key>CountryMinVersions</key>
|
||||
<dict>
|
||||
<key>OTHER</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
<key>CurrentVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>NoResultErrorIsSuccess</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>7618</key>
|
||||
<dict>
|
||||
<key>AddCountyIfNeeded</key>
|
||||
<true/>
|
||||
<key>CountryMinVersions</key>
|
||||
<dict>
|
||||
<key>OTHER</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
<key>CurrentVersion</key>
|
||||
<integer>10</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 214 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 157 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 260 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 272 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 276 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 297 KiB |
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>DatabaseMinorVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>DatabaseVersion</key>
|
||||
<integer>112</integer>
|
||||
<key>HistoricalMarker</key>
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>403</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>E3E46F2A-7168-4973-AB3E-5848F80BFC7D</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
<integer>2622</integer>
|
||||
</dict>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
<integer>2622</integer>
|
||||
<key>MetaSchemaVersion</key>
|
||||
<integer>2</integer>
|
||||
<key>SnapshotComplete</key>
|
||||
<true/>
|
||||
<key>SnapshotCompletedDate</key>
|
||||
<date>2019-08-24T02:50:48Z</date>
|
||||
<key>SnapshotTables</key>
|
||||
<dict/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
After Width: | Height: | Size: 1.2 MiB |
@@ -3,8 +3,8 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
|
||||
<date>2019-07-26T20:15:18Z</date>
|
||||
<date>2019-07-28T01:23:52Z</date>
|
||||
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
|
||||
<date>2019-07-26T20:15:18Z</date>
|
||||
<date>2019-07-28T01:23:52Z</date>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict/>
|
||||
<dict>
|
||||
<key>SuggestedMeIdentifier</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LithiumMessageTracer</key>
|
||||
<dict>
|
||||
<key>LastReportedDate</key>
|
||||
<date>2019-07-27T12:01:15Z</date>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -3,7 +3,7 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LastHistoryRowId</key>
|
||||
<integer>545</integer>
|
||||
<integer>615</integer>
|
||||
<key>LibraryBuildTag</key>
|
||||
<string>BEA5F0E8-BA6B-4462-8F73-3E53BBE4C943</string>
|
||||
<key>LibrarySchemaVersion</key>
|
||||
|
||||