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:
parent
ba1a2b32ad
commit
ede56ffc31
176
README.md
176
README.md
@ -11,7 +11,7 @@
|
||||
* [Example uses of the module](#example-uses-of-the-module)
|
||||
* [Module Interface](#module-interface)
|
||||
+ [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 a specific Photos library](#open-a-specific-photos-library)
|
||||
- [`keywords`](#keywords)
|
||||
@ -68,6 +68,7 @@
|
||||
* [Implementation Notes](#implementation-notes)
|
||||
* [Dependencies](#dependencies)
|
||||
* [Acknowledgements](#acknowledgements)
|
||||
|
||||
## 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.
|
||||
@ -123,7 +124,7 @@ To get help on a specific command, use `osxphotos help <command_name>`
|
||||
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.
|
||||
Optionally, query the Photos database using 1 or more search options; if
|
||||
@ -132,81 +133,101 @@ Usage: osxphotos help [OPTIONS] DEST
|
||||
photos will be exported.
|
||||
|
||||
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).
|
||||
--title TEXT Search for TEXT in title of photo.
|
||||
--no-title Search for photos with no title.
|
||||
--description TEXT Search for TEXT in description of photo.
|
||||
--no-description Search for photos with no description.
|
||||
--uti TEXT Search for photos whose uniform type identifier (UTI)
|
||||
matches TEXT
|
||||
-i, --ignore-case Case insensitive search for title or description. Does
|
||||
not apply to keyword, person, or album.
|
||||
--edited Search for photos that have been edited.
|
||||
--external-edit Search for photos edited in external editor.
|
||||
--favorite Search for photos marked favorite.
|
||||
--not-favorite Search for photos not marked favorite.
|
||||
--hidden Search for photos marked hidden.
|
||||
--not-hidden Search for photos not marked hidden.
|
||||
--burst Search for photos that were taken in a burst.
|
||||
--not-burst Search for photos that are not part of a burst.
|
||||
--live Search for Apple live photos
|
||||
--not-live Search for photos that are not Apple live photos
|
||||
--shared Search for photos in shared iCloud album (Photos 5
|
||||
only).
|
||||
--not-shared Search for photos not in shared iCloud album (Photos 5
|
||||
only).
|
||||
-V, --verbose Print verbose output.
|
||||
--overwrite Overwrite existing files. Default behavior is to add
|
||||
(1), (2), etc to filename if file already exists. Use
|
||||
this with caution as it may create name collisions on
|
||||
export. (e.g. if two files happen to have the same name)
|
||||
--export-by-date Automatically create output folders to organize photos
|
||||
by date created (e.g. DEST/2019/12/20/photoname.jpg).
|
||||
--export-edited Also export edited version of photo if an edited version
|
||||
exists. Edited photo will be named in form of
|
||||
"photoname_edited.ext"
|
||||
--export-bursts If a photo is a burst photo export all associated burst
|
||||
images in the library.
|
||||
--export-live If a photo is a live photo export the associated live
|
||||
video component. Live video will have same name as
|
||||
photo but with .mov extension.
|
||||
--original-name Use photo's original filename instead of current
|
||||
filename for export.
|
||||
--sidecar Create json sidecar for each photo exported in format
|
||||
useable by exiftool (https://exiftool.org/) The sidecar
|
||||
file can be used to apply metadata to the file with
|
||||
exiftool, for example: "exiftool -j=photoname.jpg.json
|
||||
photoname.jpg" The sidecar file is named in format
|
||||
photoname.ext.json where ext is extension of the photo
|
||||
(e.g. jpg).
|
||||
--only-movies Search only for movies (default searches both images and
|
||||
movies).
|
||||
--only-photos Search only for photos/images (default searches both
|
||||
images and movies).
|
||||
--download-missing Attempt to download missing photos from iCloud. The
|
||||
current implementation uses Applescript to interact with
|
||||
Photos to export the photo which will force Photos to
|
||||
download from iCloud if 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.
|
||||
--db <Photos database path> Specify Photos database path.
|
||||
--keyword TEXT Search for keyword(s).
|
||||
--person TEXT Search for person(s).
|
||||
--album TEXT Search for album(s).
|
||||
--uuid TEXT Search for UUID(s).
|
||||
--title TEXT Search for TEXT in title of photo.
|
||||
--no-title Search for photos with no title.
|
||||
--description TEXT Search for TEXT in description of photo.
|
||||
--no-description Search for photos with no description.
|
||||
--uti TEXT Search for photos whose uniform type identifier
|
||||
(UTI) matches TEXT
|
||||
-i, --ignore-case Case insensitive search for title or
|
||||
description. Does not apply to keyword, person,
|
||||
or album.
|
||||
--edited Search for photos that have been edited.
|
||||
--external-edit Search for photos edited in external editor.
|
||||
--favorite Search for photos marked favorite.
|
||||
--not-favorite Search for photos not marked favorite.
|
||||
--hidden Search for photos marked hidden.
|
||||
--not-hidden Search for photos not marked hidden.
|
||||
--burst Search for photos that were taken in a burst.
|
||||
--not-burst Search for photos that are not part of a burst.
|
||||
--live Search for Apple live photos
|
||||
--not-live Search for photos that are not Apple live
|
||||
photos
|
||||
--shared Search for photos in shared iCloud album
|
||||
(Photos 5 only).
|
||||
--not-shared Search for photos not in shared iCloud album
|
||||
(Photos 5 only).
|
||||
-V, --verbose Print verbose output.
|
||||
--overwrite Overwrite existing files. Default behavior is
|
||||
to add (1), (2), etc to filename if file
|
||||
already exists. Use this with caution as it may
|
||||
create name collisions on export. (e.g. if two
|
||||
files happen to have the same name)
|
||||
--export-by-date Automatically create output folders to organize
|
||||
photos by date created (e.g.
|
||||
DEST/2019/12/20/photoname.jpg).
|
||||
--export-edited Also export edited version of photo if an
|
||||
edited version exists. Edited photo will be
|
||||
named in form of "photoname_edited.ext"
|
||||
--export-bursts If a photo is a burst photo export all
|
||||
associated burst images in the library.
|
||||
--export-live If a photo is a live photo export the
|
||||
associated live video component. Live video
|
||||
will have same name as photo but with .mov
|
||||
extension.
|
||||
--original-name Use photo's original filename instead of
|
||||
current filename for export.
|
||||
--sidecar Create JSON sidecar for each photo exported in
|
||||
format useable by exiftool
|
||||
(https://exiftool.org/) The sidecar file can be
|
||||
used to apply metadata to the file with
|
||||
exiftool, for example: "exiftool
|
||||
-j=photoname.jpg.json photoname.jpg" The
|
||||
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).
|
||||
--only-photos Search only for photos/images (default searches
|
||||
both images and movies).
|
||||
--download-missing Attempt to download missing photos from iCloud.
|
||||
The current implementation uses Applescript to
|
||||
interact with Photos to export the photo which
|
||||
will force Photos to download from iCloud if
|
||||
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.
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
`osxphotos query --keyword Kids --json >results.json`
|
||||
`osxphotos query --keyword Kids --json ~/Pictures/Photos\ Library.photoslibrary >results.json`
|
||||
|
||||
## Example uses of the module
|
||||
|
||||
```python
|
||||
import os.path
|
||||
|
||||
import osxphotos
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
print(photosdb.keywords)
|
||||
print(photosdb.persons)
|
||||
print(photosdb.albums)
|
||||
@ -245,12 +266,12 @@ if __name__ == "__main__":
|
||||
otherwise, export the original version """
|
||||
|
||||
import os.path
|
||||
|
||||
import osxphotos
|
||||
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
photos = photosdb.photos()
|
||||
|
||||
export_path = os.path.expanduser("~/Desktop/export")
|
||||
@ -274,17 +295,16 @@ if __name__ == "__main__":
|
||||
|
||||
### PhotosDB
|
||||
|
||||
#### Open the default Photos library
|
||||
#### Read a Photos library database
|
||||
|
||||
```python
|
||||
osxphotos.PhotosDB()
|
||||
osxphotos.PhotosDB(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.
|
||||
|
||||
@ -292,8 +312,7 @@ Open the default (last opened) Photos library. (E.g. this is the library that wo
|
||||
|
||||
```python
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
photosdb = osxphotos.PhotosDB(osxphotos.utils.get_last_library_path())
|
||||
```
|
||||
|
||||
#### 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
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB()
|
||||
>>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
>>> photos = photosdb.photos()
|
||||
>>> len(photos)
|
||||
25002
|
||||
@ -627,7 +646,7 @@ Example below gets list of all photos that are bursts, selects one of of them an
|
||||
|
||||
```python
|
||||
>>> import osxphotos
|
||||
>>> photosdb = osxphotos.PhotosDB()
|
||||
>>> photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
>>> bursts = [p for p in photosdb.photos() if p.burst]
|
||||
>>> burst_photo = bursts[5]
|
||||
>>> 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
|
||||
import osxphotos
|
||||
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
photos = photosdb.photos()
|
||||
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
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
|
||||
photosdb = osxphotos.PhotosDB("/Users/smith/Pictures/Photos Library.photoslibrary")
|
||||
print(f"db file = {photosdb.db_path}")
|
||||
print(f"db version = {photosdb.db_version}")
|
||||
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
import osxphotos
|
||||
import os.path
|
||||
|
||||
|
||||
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 version = {photosdb.db_version}")
|
||||
|
||||
|
||||
@ -8,7 +8,8 @@ import osxphotos
|
||||
|
||||
|
||||
def main():
|
||||
photosdb = osxphotos.PhotosDB()
|
||||
db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary")
|
||||
photosdb = osxphotos.PhotosDB(db)
|
||||
photos = photosdb.photos()
|
||||
|
||||
export_path = os.path.expanduser("~/Desktop/export")
|
||||
|
||||
@ -19,6 +19,42 @@ from .utils import create_path_by_date, _copy_file
|
||||
# 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:
|
||||
def __init__(self, db=None, json=False, debug=False):
|
||||
if debug:
|
||||
@ -36,7 +72,8 @@ CTX_SETTINGS = dict(help_option_names=["-h", "--help"])
|
||||
required=False,
|
||||
metavar="<Photos database path>",
|
||||
default=None,
|
||||
help="Specify database file.",
|
||||
help="Specify Photos database path.",
|
||||
type=click.Path(exists=True),
|
||||
)
|
||||
@click.option(
|
||||
"--json",
|
||||
@ -53,49 +90,153 @@ def cli(ctx, db, json, debug):
|
||||
|
||||
|
||||
@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
|
||||
def keywords(cli_obj):
|
||||
@click.pass_context
|
||||
def keywords(ctx, cli_obj, db, json_, 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}
|
||||
if cli_obj.json:
|
||||
if json_ or cli_obj.json:
|
||||
click.echo(json.dumps(keywords))
|
||||
else:
|
||||
click.echo(yaml.dump(keywords, sort_keys=False))
|
||||
|
||||
|
||||
@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
|
||||
def albums(cli_obj):
|
||||
@click.pass_context
|
||||
def albums(ctx, cli_obj, db, json_, 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}
|
||||
if photosdb.db_version >= _PHOTOS_5_VERSION:
|
||||
albums["shared albums"] = photosdb.albums_shared_as_dict
|
||||
|
||||
if cli_obj.json:
|
||||
if json_ or cli_obj.json:
|
||||
click.echo(json.dumps(albums))
|
||||
else:
|
||||
click.echo(yaml.dump(albums, sort_keys=False))
|
||||
|
||||
|
||||
@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
|
||||
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. """
|
||||
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}
|
||||
if cli_obj.json:
|
||||
if json_ or cli_obj.json:
|
||||
click.echo(json.dumps(persons))
|
||||
else:
|
||||
click.echo(yaml.dump(persons, sort_keys=False))
|
||||
|
||||
|
||||
@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
|
||||
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. """
|
||||
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["database_path"] = pdb.db_path
|
||||
info["database_version"] = pdb.db_version
|
||||
@ -146,61 +287,116 @@ def info(cli_obj):
|
||||
info["persons_count"] = len(persons)
|
||||
info["persons"] = persons
|
||||
|
||||
if cli_obj.json:
|
||||
if cli_obj.json or json_:
|
||||
click.echo(json.dumps(info))
|
||||
else:
|
||||
click.echo(yaml.dump(info, sort_keys=False))
|
||||
|
||||
|
||||
@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_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,
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Print output in JSON format.",
|
||||
)
|
||||
@click.pass_obj
|
||||
def dump(cli_obj, json):
|
||||
""" Print list of all photos & associated info from the Photos library. """
|
||||
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):
|
||||
@click.pass_context
|
||||
def list_libraries(ctx, cli_obj, json_):
|
||||
""" Print list of Photos libraries found on the system. """
|
||||
photo_libs = osxphotos.utils.list_photo_libraries()
|
||||
sys_lib = None
|
||||
_, major, _ = osxphotos.utils._get_os_version()
|
||||
if int(major) >= 15:
|
||||
sys_lib = osxphotos.utils.get_system_library_path()
|
||||
_list_libraries(json_=json_ or cli_obj.json)
|
||||
|
||||
|
||||
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_flag = sys_lib_flag = False
|
||||
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
|
||||
|
||||
for lib in photo_libs:
|
||||
if lib == sys_lib:
|
||||
click.echo(f"(*)\t{lib}")
|
||||
sys_lib_flag = True
|
||||
elif lib == last_lib:
|
||||
click.echo(f"(#)\t{lib}")
|
||||
last_lib_flag = True
|
||||
else:
|
||||
click.echo(f"\t{lib}")
|
||||
for lib in photo_libs:
|
||||
if lib == sys_lib:
|
||||
click.echo(f"(*)\t{lib}")
|
||||
sys_lib_flag = True
|
||||
elif lib == last_lib:
|
||||
click.echo(f"(#)\t{lib}")
|
||||
last_lib_flag = True
|
||||
else:
|
||||
click.echo(f"\t{lib}")
|
||||
|
||||
if sys_lib_flag or last_lib_flag:
|
||||
click.echo("\n")
|
||||
if sys_lib_flag:
|
||||
click.echo("(*)\tSystem Photos Library")
|
||||
if last_lib_flag:
|
||||
click.echo("(#)\tLast opened Photos Library")
|
||||
if sys_lib_flag or last_lib_flag:
|
||||
click.echo("\n")
|
||||
if sys_lib_flag:
|
||||
click.echo("(*)\tSystem Photos Library")
|
||||
if last_lib_flag:
|
||||
click.echo("(#)\tLast opened Photos Library")
|
||||
|
||||
|
||||
@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("--person", default=None, multiple=True, help="Search for person(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,
|
||||
help="Search only for photos/images (default searches both images and movies).",
|
||||
)
|
||||
@click.option(
|
||||
"--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 query(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
photos_library,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
@ -317,7 +509,7 @@ def query(
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
json,
|
||||
json_,
|
||||
edited,
|
||||
external_edit,
|
||||
favorite,
|
||||
@ -429,8 +621,15 @@ def query(
|
||||
if only_photos:
|
||||
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(
|
||||
cli_obj,
|
||||
db,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
@ -440,7 +639,6 @@ def query(
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
json,
|
||||
edited,
|
||||
external_edit,
|
||||
favorite,
|
||||
@ -463,10 +661,18 @@ def query(
|
||||
incloud,
|
||||
not_incloud,
|
||||
)
|
||||
print_photo_info(photos, cli_obj.json or json)
|
||||
print_photo_info(photos, cli_obj.json or json_)
|
||||
|
||||
|
||||
@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("--person", default=None, multiple=True, help="Search for person(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. "
|
||||
"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_context
|
||||
def export(
|
||||
ctx,
|
||||
cli_obj,
|
||||
db,
|
||||
photos_library,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
@ -679,8 +888,15 @@ def export(
|
||||
if only_photos:
|
||||
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(
|
||||
cli_obj,
|
||||
db,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
@ -690,7 +906,6 @@ def export(
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
json,
|
||||
edited,
|
||||
external_edit,
|
||||
favorite,
|
||||
@ -708,10 +923,10 @@ def export(
|
||||
not_burst,
|
||||
live,
|
||||
not_live,
|
||||
False, # cloudasset
|
||||
False, # not_cloudasset
|
||||
False, # incloud
|
||||
False # not_incloud
|
||||
False, # cloudasset
|
||||
False, # not_cloudasset
|
||||
False, # incloud
|
||||
False, # not_incloud
|
||||
)
|
||||
|
||||
if photos:
|
||||
@ -739,7 +954,7 @@ def export(
|
||||
export_edited,
|
||||
original_name,
|
||||
export_live,
|
||||
download_missing
|
||||
download_missing,
|
||||
)
|
||||
else:
|
||||
for p in photos:
|
||||
@ -753,7 +968,7 @@ def export(
|
||||
export_edited,
|
||||
original_name,
|
||||
export_live,
|
||||
download_missing
|
||||
download_missing,
|
||||
)
|
||||
if export_path:
|
||||
click.echo(f"Exported {p.filename} to {export_path}")
|
||||
@ -855,7 +1070,7 @@ def print_photo_info(photos, json=False):
|
||||
|
||||
|
||||
def _query(
|
||||
cli_obj,
|
||||
db,
|
||||
keyword,
|
||||
person,
|
||||
album,
|
||||
@ -865,7 +1080,6 @@ def _query(
|
||||
description,
|
||||
no_description,
|
||||
ignore_case,
|
||||
json,
|
||||
edited,
|
||||
external_edit,
|
||||
favorite,
|
||||
@ -895,7 +1109,7 @@ def _query(
|
||||
|
||||
# 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(
|
||||
keywords=keyword,
|
||||
persons=person,
|
||||
@ -1031,7 +1245,9 @@ def export_photo(
|
||||
)
|
||||
return None
|
||||
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
|
||||
|
||||
filename = None
|
||||
@ -1089,7 +1305,7 @@ def export_photo(
|
||||
|
||||
_copy_file(src_live, str(dest_live))
|
||||
else:
|
||||
click.echo(f"Skipping missing live movie for {filename}")
|
||||
click.echo(f"Skipping missing live movie for {filename}")
|
||||
|
||||
return photo_path
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
""" version info """
|
||||
|
||||
__version__ = "0.21.6"
|
||||
__version__ = "0.22.0"
|
||||
|
||||
@ -39,11 +39,11 @@ from .utils import _check_file_exists, _get_os_version, get_last_library_path, _
|
||||
class PhotosDB:
|
||||
""" Processes a Photos.app library database to extract information about photos """
|
||||
|
||||
def __init__(self, *args, dbfile=None):
|
||||
""" create a new PhotosDB object """
|
||||
""" 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 """
|
||||
""" optional: specify path to photos library or photos.db using named argument dbfile=path """
|
||||
def __init__(self, *dbfile_, dbfile=None):
|
||||
""" create a new PhotosDB object
|
||||
path to photos library or database may be specified EITHER as first argument or as named argument dbfile=path
|
||||
specify full path to photos library or photos.db as first argument
|
||||
specify path to photos library or photos.db using named argument dbfile=path """
|
||||
|
||||
# Check OS version
|
||||
system = platform.system()
|
||||
@ -89,28 +89,28 @@ class PhotosDB:
|
||||
logging.debug(f"dbfile = {dbfile}")
|
||||
|
||||
# get the path to photos library database
|
||||
if args:
|
||||
if dbfile_:
|
||||
# got a library path as argument
|
||||
if dbfile:
|
||||
# shouldn't pass via both *args and dbfile=
|
||||
raise TypeError(
|
||||
f"photos database path must be specified as argument or named parameter dbfile but not both: args: {args}, dbfile: {dbfile}",
|
||||
args,
|
||||
dbfile_,
|
||||
dbfile,
|
||||
)
|
||||
elif len(args) == 1:
|
||||
dbfile = args[0]
|
||||
elif len(dbfile_) == 1:
|
||||
dbfile = dbfile_[0]
|
||||
else:
|
||||
raise TypeError(
|
||||
f"__init__ takes only a single argument (photos database path): {args}",
|
||||
args,
|
||||
f"__init__ takes only a single argument (photos database path): {dbfile_}",
|
||||
dbfile_,
|
||||
)
|
||||
elif dbfile is None:
|
||||
# no args and dbfile not passed, try to get last opened library
|
||||
library_path = get_last_library_path()
|
||||
if not library_path:
|
||||
raise FileNotFoundError("could not get library path")
|
||||
dbfile = os.path.join(library_path, "database/photos.db")
|
||||
raise TypeError(
|
||||
f"photos database path must be specified as argument or named parameter dbfile",
|
||||
dbfile_,
|
||||
dbfile,
|
||||
)
|
||||
|
||||
if os.path.isdir(dbfile):
|
||||
# passed a directory, assume it's a photoslibrary
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user