Refactored PhotosDB and CLI to require explicity passing the database to avoid non-deterministic behavior when last database can't be found. This may break existing code.

This commit is contained in:
Rhet Turnbull
2020-01-17 16:32:10 -08:00
parent ba1a2b32ad
commit ede56ffc31
6 changed files with 409 additions and 166 deletions

136
README.md
View File

@@ -11,7 +11,7 @@
* [Example uses of the module](#example-uses-of-the-module) * [Example uses of the module](#example-uses-of-the-module)
* [Module Interface](#module-interface) * [Module Interface](#module-interface)
+ [PhotosDB](#photosdb) + [PhotosDB](#photosdb)
- [Open the default Photos library](#open-the-default-photos-library) - [Read a Photos library database](#read-a-photos-library-database)
- [Open System Photos library](#open-system-photos-library) - [Open System Photos library](#open-system-photos-library)
- [Open a specific Photos library](#open-a-specific-photos-library) - [Open a specific Photos library](#open-a-specific-photos-library)
- [`keywords`](#keywords) - [`keywords`](#keywords)
@@ -68,6 +68,7 @@
* [Implementation Notes](#implementation-notes) * [Implementation Notes](#implementation-notes)
* [Dependencies](#dependencies) * [Dependencies](#dependencies)
* [Acknowledgements](#acknowledgements) * [Acknowledgements](#acknowledgements)
## What is osxphotos? ## What is osxphotos?
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 on your Mac--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. 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 on your Mac--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.
@@ -123,7 +124,7 @@ To get help on a specific command, use `osxphotos help <command_name>`
Example: `osxphotos help export` Example: `osxphotos help export`
``` ```
Usage: osxphotos help [OPTIONS] DEST Usage: osxphotos help [OPTIONS] [PHOTOS_LIBRARY]... DEST
Export photos from the Photos database. Export path DEST is required. Export photos from the Photos database. Export path DEST is required.
Optionally, query the Photos database using 1 or more search options; if Optionally, query the Photos database using 1 or more search options; if
@@ -132,6 +133,7 @@ Usage: osxphotos help [OPTIONS] DEST
photos will be exported. photos will be exported.
Options: Options:
--db <Photos database path> Specify Photos database path.
--keyword TEXT Search for keyword(s). --keyword TEXT Search for keyword(s).
--person TEXT Search for person(s). --person TEXT Search for person(s).
--album TEXT Search for album(s). --album TEXT Search for album(s).
@@ -140,10 +142,11 @@ Options:
--no-title Search for photos with no title. --no-title Search for photos with no title.
--description TEXT Search for TEXT in description of photo. --description TEXT Search for TEXT in description of photo.
--no-description Search for photos with no description. --no-description Search for photos with no description.
--uti TEXT Search for photos whose uniform type identifier (UTI) --uti TEXT Search for photos whose uniform type identifier
matches TEXT (UTI) matches TEXT
-i, --ignore-case Case insensitive search for title or description. Does -i, --ignore-case Case insensitive search for title or
not apply to keyword, person, or album. description. Does not apply to keyword, person,
or album.
--edited Search for photos that have been edited. --edited Search for photos that have been edited.
--external-edit Search for photos edited in external editor. --external-edit Search for photos edited in external editor.
--favorite Search for photos marked favorite. --favorite Search for photos marked favorite.
@@ -153,60 +156,78 @@ Options:
--burst Search for photos that were taken in a burst. --burst Search for photos that were taken in a burst.
--not-burst Search for photos that are not part of a burst. --not-burst Search for photos that are not part of a burst.
--live Search for Apple live photos --live Search for Apple live photos
--not-live Search for photos that are not Apple live photos --not-live Search for photos that are not Apple live
--shared Search for photos in shared iCloud album (Photos 5 photos
only). --shared Search for photos in shared iCloud album
--not-shared Search for photos not in shared iCloud album (Photos 5 (Photos 5 only).
only). --not-shared Search for photos not in shared iCloud album
(Photos 5 only).
-V, --verbose Print verbose output. -V, --verbose Print verbose output.
--overwrite Overwrite existing files. Default behavior is to add --overwrite Overwrite existing files. Default behavior is
(1), (2), etc to filename if file already exists. Use to add (1), (2), etc to filename if file
this with caution as it may create name collisions on already exists. Use this with caution as it may
export. (e.g. if two files happen to have the same name) create name collisions on export. (e.g. if two
--export-by-date Automatically create output folders to organize photos files happen to have the same name)
by date created (e.g. DEST/2019/12/20/photoname.jpg). --export-by-date Automatically create output folders to organize
--export-edited Also export edited version of photo if an edited version photos by date created (e.g.
exists. Edited photo will be named in form of DEST/2019/12/20/photoname.jpg).
"photoname_edited.ext" --export-edited Also export edited version of photo if an
--export-bursts If a photo is a burst photo export all associated burst edited version exists. Edited photo will be
images in the library. named in form of "photoname_edited.ext"
--export-live If a photo is a live photo export the associated live --export-bursts If a photo is a burst photo export all
video component. Live video will have same name as associated burst images in the library.
photo but with .mov extension. --export-live If a photo is a live photo export the
--original-name Use photo's original filename instead of current associated live video component. Live video
filename for export. will have same name as photo but with .mov
--sidecar Create json sidecar for each photo exported in format extension.
useable by exiftool (https://exiftool.org/) The sidecar --original-name Use photo's original filename instead of
file can be used to apply metadata to the file with current filename for export.
exiftool, for example: "exiftool -j=photoname.jpg.json --sidecar Create JSON sidecar for each photo exported in
photoname.jpg" The sidecar file is named in format format useable by exiftool
photoname.ext.json where ext is extension of the photo (https://exiftool.org/) The sidecar file can be
(e.g. jpg). used to apply metadata to the file with
--only-movies Search only for movies (default searches both images and exiftool, for example: "exiftool
movies). -j=photoname.jpg.json photoname.jpg" The
--only-photos Search only for photos/images (default searches both sidecar file is named in format
photoname.ext.json where ext is extension of
the photo (e.g. jpg). Note: this does not
create an XMP sidecar as used by Lightroom,
etc.
--only-movies Search only for movies (default searches both
images and movies). images and movies).
--download-missing Attempt to download missing photos from iCloud. The --only-photos Search only for photos/images (default searches
current implementation uses Applescript to interact with both images and movies).
Photos to export the photo which will force Photos to --download-missing Attempt to download missing photos from iCloud.
download from iCloud if the photo does not exist on The current implementation uses Applescript to
disk. This will be slow and will require internet interact with Photos to export the photo which
connection. This obviously only works if the Photos will force Photos to download from iCloud if
library is synched to iCloud. the photo does not exist on disk. This will be
slow and will require internet connection. This
obviously only works if the Photos library is
synched to iCloud.
-h, --help Show this message and exit. -h, --help Show this message and exit.
``` ```
Example: export all photos to ~/Desktop/export, including edited versions and live photo movies, group in folders by date created
`osxphotos export --export-edited --export-live --export-by-date ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
**Note**: Photos library/database path can also be specified using --db option:
`osxphotos export --export-edited --export-live --export-by-date --db ~/Pictures/Photos\ Library.photoslibrary ~/Desktop/export`
Example: find all photos with keyword "Kids" and output results to json file named results.json: Example: find all photos with keyword "Kids" and output results to json file named results.json:
`osxphotos query --keyword Kids --json >results.json` `osxphotos query --keyword Kids --json ~/Pictures/Photos\ Library.photoslibrary >results.json`
## Example uses of the module ## Example uses of the module
```python ```python
import os.path
import osxphotos import osxphotos
def main(): def main():
photosdb = osxphotos.PhotosDB() db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
photosdb = osxphotos.PhotosDB(db)
print(photosdb.keywords) print(photosdb.keywords)
print(photosdb.persons) print(photosdb.persons)
print(photosdb.albums) print(photosdb.albums)
@@ -245,12 +266,12 @@ if __name__ == "__main__":
otherwise, export the original version """ otherwise, export the original version """
import os.path import os.path
import osxphotos import osxphotos
def main(): def main():
photosdb = osxphotos.PhotosDB() db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
photosdb = osxphotos.PhotosDB(db)
photos = photosdb.photos() photos = photosdb.photos()
export_path = os.path.expanduser("~/Desktop/export") export_path = os.path.expanduser("~/Desktop/export")
@@ -274,17 +295,16 @@ if __name__ == "__main__":
### PhotosDB ### PhotosDB
#### Open the default Photos library #### Read a Photos library database
```python ```python
osxphotos.PhotosDB()
osxphotos.PhotosDB(path) osxphotos.PhotosDB(path)
osxphotos.PhotosDB(dbfile=path) osxphotos.PhotosDB(dbfile=path)
``` ```
Opens the Photos library database and returns a PhotosDB object. Reads the Photos library database and returns a PhotosDB object.
Optionally, pass the path to a specific database file or a Photos library (e.g. "/Users/smith/Pictures/Photos Library.photoslibrary" or "/Users/smith/Pictures/Photos Library.photoslibrary/database/photos.db"). Path to photos library may be passed **either** as first argument **or** as named argument `dbfile`. If path is not passed, PhotosDB will attempt to open the default Photos library (that is, the last library that was opened in Photos.app which may or may not also be the System Photos Library). **Note**: Users may specify a different library to open by holding down the *option* key while opening Photos.app. Pass the path to a Photos library or to a specific database file (e.g. "/Users/smith/Pictures/Photos Library.photoslibrary" or "/Users/smith/Pictures/Photos Library.photoslibrary/database/photos.db"). Normally, it's recommended you pass the path the .photoslibrary folder, not the actual database path. The latter option is provided for debugging -- e.g. for reading a database file if you don't have the entire library. Path to photos library may be passed **either** as first argument **or** as named argument `dbfile`. **Note**: In Photos, users may specify a different library to open by holding down the *option* key while opening Photos.app. See also [get_last_library_path](#get_last_library_path) and [get_system_library_path](#get_system_library_path)
If an invalid path is passed, PhotosDB will raise `ValueError` exception. If an invalid path is passed, PhotosDB will raise `ValueError` exception.
@@ -292,8 +312,7 @@ Open the default (last opened) Photos library. (E.g. this is the library that wo
```python ```python
import osxphotos import osxphotos
photosdb = osxphotos.PhotosDB(osxphotos.utils.get_last_library_path())
photosdb = osxphotos.PhotosDB()
``` ```
#### Open System Photos library #### Open System Photos library
@@ -516,7 +535,7 @@ For example, in my library, Photos says I have 19,386 photos and 474 movies. Ho
```python ```python
>>> import osxphotos >>> import osxphotos
>>> photosdb = osxphotos.PhotosDB() >>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
>>> photos = photosdb.photos() >>> photos = photosdb.photos()
>>> len(photos) >>> len(photos)
25002 25002
@@ -627,7 +646,7 @@ Example below gets list of all photos that are bursts, selects one of of them an
```python ```python
>>> import osxphotos >>> import osxphotos
>>> photosdb = osxphotos.PhotosDB() >>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
>>> bursts = [p for p in photosdb.photos() if p.burst] >>> bursts = [p for p in photosdb.photos() if p.burst]
>>> burst_photo = bursts[5] >>> burst_photo = bursts[5]
>>> len(burst_photo.burst_photos) >>> len(burst_photo.burst_photos)
@@ -671,7 +690,7 @@ The json sidecar file can be used by exiftool to apply the metadata from the jso
```python ```python
import osxphotos import osxphotos
photosdb = osxphotos.PhotosDB() photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
photos = photosdb.photos() photos = photosdb.photos()
photos[0].export("/tmp","photo_name.jpg",sidecar=True) photos[0].export("/tmp","photo_name.jpg",sidecar=True)
``` ```
@@ -721,7 +740,8 @@ Checks to see if path exists, if it does, do nothing and return path. If path do
import osxphotos import osxphotos
def main(): def main():
photosdb = osxphotos.PhotosDB()
photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
print(f"db file = {photosdb.db_path}") print(f"db file = {photosdb.db_path}")
print(f"db version = {photosdb.db_version}") print(f"db version = {photosdb.db_version}")

View File

@@ -1,8 +1,14 @@
import osxphotos import osxphotos
import os.path
def main(): def main():
photosdb = osxphotos.PhotosDB() db = osxphotos.utils.get_system_library_path()
if db is None:
# Note: get_system_library_path only works on MacOS 10.15+
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
photosdb = osxphotos.PhotosDB(db)
print(f"db file = {photosdb.db_path}") print(f"db file = {photosdb.db_path}")
print(f"db version = {photosdb.db_version}") print(f"db version = {photosdb.db_version}")

View File

@@ -8,7 +8,8 @@ import osxphotos
def main(): def main():
photosdb = osxphotos.PhotosDB() db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
photosdb = osxphotos.PhotosDB(db)
photos = photosdb.photos() photos = photosdb.photos()
export_path = os.path.expanduser("~/Desktop/export") export_path = os.path.expanduser("~/Desktop/export")

View File

@@ -19,6 +19,42 @@ from .utils import create_path_by_date, _copy_file
# TODO: add search for filename # TODO: add search for filename
def get_photos_db(*db_options):
""" Return path to photos db, select first non-None arg
"""
if db_options:
for db in db_options:
if db is not None:
return db
# _list_libraries()
return None
# if get here, no valid database paths passed, so ask user
# _, major, _ = osxphotos.utils._get_os_version()
# last_lib = osxphotos.utils.get_last_library_path()
# if last_lib is not None:
# db = last_lib
# return db
# sys_lib = None
# if int(major) >= 15:
# sys_lib = osxphotos.utils.get_system_library_path()
# if sys_lib is not None:
# db = sys_lib
# return db
# db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
# if os.path.isdir(db):
# return db
# else:
# return None ### TODO: put list here
# Click CLI object & context settings
class CLI_Obj: class CLI_Obj:
def __init__(self, db=None, json=False, debug=False): def __init__(self, db=None, json=False, debug=False):
if debug: if debug:
@@ -36,7 +72,8 @@ CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
required=False, required=False,
metavar="<Photos database path>", metavar="<Photos database path>",
default=None, default=None,
help="Specify database file.", help="Specify Photos database path.",
type=click.Path(exists=True),
) )
@click.option( @click.option(
"--json", "--json",
@@ -53,49 +90,153 @@ def cli(ctx, db, json, debug):
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.pass_obj @click.pass_obj
def keywords(cli_obj): @click.pass_context
def keywords(ctx, cli_obj, db, json_, photos_library):
""" Print out keywords found in the Photos library. """ """ Print out keywords found in the Photos library. """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["keywords"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
keywords = {"keywords": photosdb.keywords_as_dict} keywords = {"keywords": photosdb.keywords_as_dict}
if cli_obj.json: if json_ or cli_obj.json:
click.echo(json.dumps(keywords)) click.echo(json.dumps(keywords))
else: else:
click.echo(yaml.dump(keywords, sort_keys=False)) click.echo(yaml.dump(keywords, sort_keys=False))
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.pass_obj @click.pass_obj
def albums(cli_obj): @click.pass_context
def albums(ctx, cli_obj, db, json_, photos_library):
""" Print out albums found in the Photos library. """ """ Print out albums found in the Photos library. """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["albums"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
albums = {"albums": photosdb.albums_as_dict} albums = {"albums": photosdb.albums_as_dict}
if photosdb.db_version >= _PHOTOS_5_VERSION: if photosdb.db_version >= _PHOTOS_5_VERSION:
albums["shared albums"] = photosdb.albums_shared_as_dict albums["shared albums"] = photosdb.albums_shared_as_dict
if cli_obj.json: if json_ or cli_obj.json:
click.echo(json.dumps(albums)) click.echo(json.dumps(albums))
else: else:
click.echo(yaml.dump(albums, sort_keys=False)) click.echo(yaml.dump(albums, sort_keys=False))
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.pass_obj @click.pass_obj
def persons(cli_obj): @click.pass_context
def persons(ctx, cli_obj, db, json_, photos_library):
""" Print out persons (faces) found in the Photos library. """ """ Print out persons (faces) found in the Photos library. """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["persons"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
photosdb = osxphotos.PhotosDB(dbfile=db)
persons = {"persons": photosdb.persons_as_dict} persons = {"persons": photosdb.persons_as_dict}
if cli_obj.json: if json_ or cli_obj.json:
click.echo(json.dumps(persons)) click.echo(json.dumps(persons))
else: else:
click.echo(yaml.dump(persons, sort_keys=False)) click.echo(yaml.dump(persons, sort_keys=False))
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.pass_obj @click.pass_obj
def info(cli_obj): @click.pass_context
def info(ctx, cli_obj, db, json_, photos_library):
""" Print out descriptive info of the Photos library database. """ """ Print out descriptive info of the Photos library database. """
pdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["info"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
pdb = osxphotos.PhotosDB(dbfile=db)
info = {} info = {}
info["database_path"] = pdb.db_path info["database_path"] = pdb.db_path
info["database_version"] = pdb.db_version info["database_version"] = pdb.db_version
@@ -146,40 +287,79 @@ def info(cli_obj):
info["persons_count"] = len(persons) info["persons_count"] = len(persons)
info["persons"] = persons info["persons"] = persons
if cli_obj.json: if cli_obj.json or json_:
click.echo(json.dumps(info)) click.echo(json.dumps(info))
else: else:
click.echo(yaml.dump(info, sort_keys=False)) click.echo(yaml.dump(info, sort_keys=False))
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option( @click.option(
"--json", "--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.pass_obj
@click.pass_context
def dump(ctx, cli_obj, db, json_, photos_library):
""" Print list of all photos & associated info from the Photos library. """
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["dump"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
pdb = osxphotos.PhotosDB(dbfile=db)
photos = pdb.photos(movies=True)
print_photo_info(photos, json_ or cli_obj.json)
@cli.command(name="list")
@click.option(
"--json",
"json_",
required=False, required=False,
is_flag=True, is_flag=True,
default=False, default=False,
help="Print output in JSON format.", help="Print output in JSON format.",
) )
@click.pass_obj @click.pass_obj
def dump(cli_obj, json): @click.pass_context
""" Print list of all photos & associated info from the Photos library. """ def list_libraries(ctx, cli_obj, json_):
pdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
photos = pdb.photos(movies=True)
print_photo_info(photos, cli_obj.json or json)
@cli.command(name="list")
@click.pass_obj
def list_libraries(cli_obj):
""" Print list of Photos libraries found on the system. """ """ Print list of Photos libraries found on the system. """
photo_libs = osxphotos.utils.list_photo_libraries() _list_libraries(json_=json_ or cli_obj.json)
sys_lib = None
_, major, _ = osxphotos.utils._get_os_version()
if int(major) >= 15:
sys_lib = osxphotos.utils.get_system_library_path()
def _list_libraries(json_=False):
""" Print list of Photos libraries found on the system.
If json_ == True, print output as JSON (default = False) """
photo_libs = osxphotos.utils.list_photo_libraries()
sys_lib = osxphotos.utils.get_system_library_path()
last_lib = osxphotos.utils.get_last_library_path() last_lib = osxphotos.utils.get_last_library_path()
if json_:
libs = {
"photo_libraries": photo_libs,
"system_library": sys_lib,
"last_library": last_lib,
}
click.echo(json.dumps(libs))
else:
last_lib_flag = sys_lib_flag = False last_lib_flag = sys_lib_flag = False
for lib in photo_libs: for lib in photo_libs:
@@ -201,6 +381,22 @@ def list_libraries(cli_obj):
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option(
"--json",
"json_",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format.",
)
@click.option("--keyword", default=None, multiple=True, help="Search for keyword(s).") @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("--person", default=None, multiple=True, help="Search for person(s).")
@click.option("--album", default=None, multiple=True, help="Search for album(s).") @click.option("--album", default=None, multiple=True, help="Search for album(s).")
@@ -296,18 +492,14 @@ def list_libraries(cli_obj):
is_flag=True, is_flag=True,
help="Search only for photos/images (default searches both images and movies).", help="Search only for photos/images (default searches both images and movies).",
) )
@click.option( @click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
"--json",
required=False,
is_flag=True,
default=False,
help="Print output in JSON format",
)
@click.pass_obj @click.pass_obj
@click.pass_context @click.pass_context
def query( def query(
ctx, ctx,
cli_obj, cli_obj,
db,
photos_library,
keyword, keyword,
person, person,
album, album,
@@ -317,7 +509,7 @@ def query(
description, description,
no_description, no_description,
ignore_case, ignore_case,
json, json_,
edited, edited,
external_edit, external_edit,
favorite, favorite,
@@ -429,8 +621,15 @@ def query(
if only_photos: if only_photos:
ismovie = False ismovie = False
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["query"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
photos = _query( photos = _query(
cli_obj, db,
keyword, keyword,
person, person,
album, album,
@@ -440,7 +639,6 @@ def query(
description, description,
no_description, no_description,
ignore_case, ignore_case,
json,
edited, edited,
external_edit, external_edit,
favorite, favorite,
@@ -463,10 +661,18 @@ def query(
incloud, incloud,
not_incloud, not_incloud,
) )
print_photo_info(photos, cli_obj.json or json) print_photo_info(photos, cli_obj.json or json_)
@cli.command() @cli.command()
@click.option(
"--db",
required=False,
metavar="<Photos database path>",
default=None,
help="Specify Photos database path.",
type=click.Path(exists=True),
)
@click.option("--keyword", default=None, multiple=True, help="Search for keyword(s).") @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("--person", default=None, multiple=True, help="Search for person(s).")
@click.option("--album", default=None, multiple=True, help="Search for album(s).") @click.option("--album", default=None, multiple=True, help="Search for album(s).")
@@ -591,12 +797,15 @@ def query(
"the photo does not exist on disk. This will be slow and will require internet connection. " "the photo does not exist on disk. This will be slow and will require internet connection. "
"This obviously only works if the Photos library is synched to iCloud.", "This obviously only works if the Photos library is synched to iCloud.",
) )
@click.argument("dest", nargs=1) @click.argument("photos_library", nargs=-1, type=click.Path(exists=True))
@click.argument("dest", nargs=1, type=click.Path(exists=True))
@click.pass_obj @click.pass_obj
@click.pass_context @click.pass_context
def export( def export(
ctx, ctx,
cli_obj, cli_obj,
db,
photos_library,
keyword, keyword,
person, person,
album, album,
@@ -679,8 +888,15 @@ def export(
if only_photos: if only_photos:
ismovie = False ismovie = False
db = get_photos_db(*photos_library, db, cli_obj.db)
if db is None:
click.echo(cli.commands["export"].get_help(ctx))
click.echo("\n\nLocated the following Photos library databases: ")
_list_libraries()
return
photos = _query( photos = _query(
cli_obj, db,
keyword, keyword,
person, person,
album, album,
@@ -690,7 +906,6 @@ def export(
description, description,
no_description, no_description,
ignore_case, ignore_case,
json,
edited, edited,
external_edit, external_edit,
favorite, favorite,
@@ -711,7 +926,7 @@ def export(
False, # cloudasset False, # cloudasset
False, # not_cloudasset False, # not_cloudasset
False, # incloud False, # incloud
False # not_incloud False, # not_incloud
) )
if photos: if photos:
@@ -739,7 +954,7 @@ def export(
export_edited, export_edited,
original_name, original_name,
export_live, export_live,
download_missing download_missing,
) )
else: else:
for p in photos: for p in photos:
@@ -753,7 +968,7 @@ def export(
export_edited, export_edited,
original_name, original_name,
export_live, export_live,
download_missing download_missing,
) )
if export_path: if export_path:
click.echo(f"Exported {p.filename} to {export_path}") click.echo(f"Exported {p.filename} to {export_path}")
@@ -855,7 +1070,7 @@ def print_photo_info(photos, json=False):
def _query( def _query(
cli_obj, db,
keyword, keyword,
person, person,
album, album,
@@ -865,7 +1080,6 @@ def _query(
description, description,
no_description, no_description,
ignore_case, ignore_case,
json,
edited, edited,
external_edit, external_edit,
favorite, favorite,
@@ -895,7 +1109,7 @@ def _query(
# TODO: this is getting too hairy -- need to change to named args # TODO: this is getting too hairy -- need to change to named args
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db) photosdb = osxphotos.PhotosDB(dbfile=db)
photos = photosdb.photos( photos = photosdb.photos(
keywords=keyword, keywords=keyword,
persons=person, persons=person,
@@ -1031,7 +1245,9 @@ def export_photo(
) )
return None return None
elif photo.ismissing and not photo.iscloudasset or not photo.incloud: elif photo.ismissing and not photo.iscloudasset or not photo.incloud:
click.echo(f"Skipping missing {photo.filename}: not iCloud asset or missing from cloud") click.echo(
f"Skipping missing {photo.filename}: not iCloud asset or missing from cloud"
)
return None return None
filename = None filename = None

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.21.6" __version__ = "0.22.0"

View File

@@ -39,11 +39,11 @@ from .utils import _check_file_exists, _get_os_version, get_last_library_path, _
class PhotosDB: class PhotosDB:
""" Processes a Photos.app library database to extract information about photos """ """ Processes a Photos.app library database to extract information about photos """
def __init__(self, *args, dbfile=None): def __init__(self, *dbfile_, dbfile=None):
""" create a new PhotosDB object """ """ create a new PhotosDB object
""" path to photos library or database may be specified EITHER as first argument or as named argument dbfile=path """ path to photos library or database may be specified EITHER as first argument or as named argument dbfile=path
""" optional: specify full path to photos library or photos.db as first argument """ specify full path to photos library or photos.db as first argument
""" optional: specify path to photos library or photos.db using named argument dbfile=path """ specify path to photos library or photos.db using named argument dbfile=path """
# Check OS version # Check OS version
system = platform.system() system = platform.system()
@@ -89,28 +89,28 @@ class PhotosDB:
logging.debug(f"dbfile = {dbfile}") logging.debug(f"dbfile = {dbfile}")
# get the path to photos library database # get the path to photos library database
if args: if dbfile_:
# got a library path as argument # got a library path as argument
if dbfile: if dbfile:
# shouldn't pass via both *args and dbfile= # shouldn't pass via both *args and dbfile=
raise TypeError( raise TypeError(
f"photos database path must be specified as argument or named parameter dbfile but not both: args: {args}, dbfile: {dbfile}", f"photos database path must be specified as argument or named parameter dbfile but not both: args: {args}, dbfile: {dbfile}",
args, dbfile_,
dbfile, dbfile,
) )
elif len(args) == 1: elif len(dbfile_) == 1:
dbfile = args[0] dbfile = dbfile_[0]
else: else:
raise TypeError( raise TypeError(
f"__init__ takes only a single argument (photos database path): {args}", f"__init__ takes only a single argument (photos database path): {dbfile_}",
args, dbfile_,
) )
elif dbfile is None: elif dbfile is None:
# no args and dbfile not passed, try to get last opened library raise TypeError(
library_path = get_last_library_path() f"photos database path must be specified as argument or named parameter dbfile",
if not library_path: dbfile_,
raise FileNotFoundError("could not get library path") dbfile,
dbfile = os.path.join(library_path, "database/photos.db") )
if os.path.isdir(dbfile): if os.path.isdir(dbfile):
# passed a directory, assume it's a photoslibrary # passed a directory, assume it's a photoslibrary