Added support for filtering only movies or photos to CLI; added search for UTI to CLI

This commit is contained in:
Rhet Turnbull
2019-12-29 08:37:38 -08:00
parent bfcab0c4fe
commit 9cd5363a80
21 changed files with 165 additions and 43 deletions

View File

@@ -18,6 +18,7 @@ from .utils import create_path_by_date
# TODO: add "--any" to search any field (e.g. keyword, description, title contains "wedding") (add case insensitive option) # TODO: add "--any" to search any field (e.g. keyword, description, title contains "wedding") (add case insensitive option)
# TODO: add search for filename # TODO: add search for filename
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:
@@ -78,7 +79,6 @@ def albums(cli_obj):
click.echo(yaml.dump(albums, sort_keys=False)) click.echo(yaml.dump(albums, sort_keys=False))
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def persons(cli_obj): def persons(cli_obj):
@@ -112,7 +112,6 @@ def info(cli_obj):
shared_movies = [p for p in movies if p.shared] shared_movies = [p for p in movies if p.shared]
info["shared_movie_count"] = len(shared_movies) info["shared_movie_count"] = len(shared_movies)
keywords = pdb.keywords_as_dict keywords = pdb.keywords_as_dict
info["keywords_count"] = len(keywords) info["keywords_count"] = len(keywords)
@@ -214,6 +213,12 @@ def list_libraries(cli_obj):
@click.option( @click.option(
"--no-description", is_flag=True, help="Search for photos with no description." "--no-description", is_flag=True, help="Search for photos with no description."
) )
@click.option(
"--uti",
default=None,
multiple=False,
help="Search for photos whose uniform type identifier (UTI) matches TEXT",
)
@click.option( @click.option(
"-i", "-i",
"--ignore-case", "--ignore-case",
@@ -237,10 +242,24 @@ def list_libraries(cli_obj):
help="Search for photos present on disk (e.g. not missing).", help="Search for photos present on disk (e.g. not missing).",
) )
@click.option( @click.option(
"--shared", is_flag=True, help="Search for photos in shared iCloud album (Photos 5 only)." "--shared",
is_flag=True,
help="Search for photos in shared iCloud album (Photos 5 only).",
) )
@click.option( @click.option(
"--not-shared", is_flag=True, help="Search for photos not in shared iCloud album (Photos 5 only)." "--not-shared",
is_flag=True,
help="Search for photos not in shared iCloud album (Photos 5 only).",
)
@click.option(
"--only-movies",
is_flag=True,
help="Search only for movies (default searches both images and movies)",
)
@click.option(
"--only-photos",
is_flag=True,
help="Search only for photos/images (default searches both images and movies)",
) )
@click.option( @click.option(
"--json", "--json",
@@ -274,6 +293,9 @@ def query(
not_missing, not_missing,
shared, shared,
not_shared, not_shared,
only_movies,
only_photos,
uti,
): ):
""" Query the Photos database using 1 or more search options; """ Query the Photos database using 1 or more search options;
if more than one option is provided, they are treated as "AND" if more than one option is provided, they are treated as "AND"
@@ -301,6 +323,9 @@ def query(
not_missing, not_missing,
shared, shared,
not_shared, not_shared,
only_movies,
only_photos,
uti,
] ]
): ):
click.echo(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
@@ -325,31 +350,45 @@ def query(
# can't search for both description and no_description # can't search for both description and no_description
click.echo(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
else: elif only_photos and only_movies:
photos = _query( # can't have only photos and only movies
cli_obj, click.echo(cli.commands["query"].get_help(ctx))
keyword, return
person,
album, # actually have something to query
uuid, isphoto = ismovie = True # default searches for everything
title, if only_movies:
no_title, isphoto = False
description, if only_photos:
no_description, ismovie = False
ignore_case,
json, photos = _query(
edited, cli_obj,
external_edit, keyword,
favorite, person,
not_favorite, album,
hidden, uuid,
not_hidden, title,
missing, no_title,
not_missing, description,
shared, no_description,
not_shared, ignore_case,
) json,
print_photo_info(photos, cli_obj.json or json) edited,
external_edit,
favorite,
not_favorite,
hidden,
not_hidden,
missing,
not_missing,
shared,
not_shared,
isphoto,
ismovie,
uti,
)
print_photo_info(photos, cli_obj.json or json)
@cli.command() @cli.command()
@@ -370,6 +409,12 @@ def query(
@click.option( @click.option(
"--no-description", is_flag=True, help="Search for photos with no description." "--no-description", is_flag=True, help="Search for photos with no description."
) )
@click.option(
"--uti",
default=None,
multiple=False,
help="Search for photos whose uniform type identifier (UTI) matches TEXT",
)
@click.option( @click.option(
"-i", "-i",
"--ignore-case", "--ignore-case",
@@ -387,10 +432,14 @@ def query(
@click.option("--hidden", is_flag=True, help="Search for photos marked hidden.") @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("--not-hidden", is_flag=True, help="Search for photos not marked hidden.")
@click.option( @click.option(
"--shared", is_flag=True, help="Search for photos in shared iCloud album (Photos 5 only)." "--shared",
is_flag=True,
help="Search for photos in shared iCloud album (Photos 5 only).",
) )
@click.option( @click.option(
"--not-shared", is_flag=True, help="Search for photos not in shared iCloud album (Photos 5 only)." "--not-shared",
is_flag=True,
help="Search for photos not in shared iCloud album (Photos 5 only).",
) )
@click.option("--verbose", is_flag=True, help="Print verbose output.") @click.option("--verbose", is_flag=True, help="Print verbose output.")
@click.option( @click.option(
@@ -427,6 +476,16 @@ def query(
'"exiftool -j=photoname.jpg.json photoname.jpg" ' '"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).", "The sidecar file is named in format photoname.ext.json where ext is extension of the photo (e.g. jpg).",
) )
@click.option(
"--only-movies",
is_flag=True,
help="Search only for movies (default searches both images and movies)",
)
@click.option(
"--only-photos",
is_flag=True,
help="Search only for photos/images (default searches both images and movies)",
)
@click.argument("dest", nargs=1) @click.argument("dest", nargs=1)
@click.pass_obj @click.pass_obj
@click.pass_context @click.pass_context
@@ -441,6 +500,7 @@ def export(
no_title, no_title,
description, description,
no_description, no_description,
uti,
ignore_case, ignore_case,
edited, edited,
external_edit, external_edit,
@@ -456,6 +516,8 @@ def export(
export_edited, export_edited,
original_name, original_name,
sidecar, sidecar,
only_photos,
only_movies,
dest, dest,
): ):
""" Export photos from the Photos database. """ Export photos from the Photos database.
@@ -471,7 +533,34 @@ def export(
if not os.path.isdir(dest): if not os.path.isdir(dest):
sys.exit("DEST must be valid path") sys.exit("DEST must be valid path")
# if no query terms, show help and return # sanity check input args
if favorite and not_favorite:
# can't search for both favorite and notfavorite
click.echo(cli.commands["export"].get_help(ctx))
return
elif hidden and not_hidden:
# can't search for both hidden and nothidden
click.echo(cli.commands["export"].get_help(ctx))
return
elif title and no_title:
# can't search for both title and no_title
click.echo(cli.commands["export"].get_help(ctx))
return
elif description and no_description:
# can't search for both description and no_description
click.echo(cli.commands["export"].get_help(ctx))
return
elif only_photos and only_movies:
# can't have only photos and only movies
click.echo(cli.commands["export"].get_help(ctx))
return
isphoto = ismovie = True # default searches for everything
if only_movies:
isphoto = False
if only_photos:
ismovie = False
photos = _query( photos = _query(
cli_obj, cli_obj,
keyword, keyword,
@@ -494,6 +583,9 @@ def export(
None, # not-missing None, # not-missing
shared, shared,
not_shared, not_shared,
isphoto,
ismovie,
uti,
) )
if photos: if photos:
@@ -579,6 +671,9 @@ def print_photo_info(photos, json=False):
"latitude", "latitude",
"longitude", "longitude",
"path_edited", "path_edited",
"isphoto",
"ismovie",
"uti",
] ]
) )
for p in photos: for p in photos:
@@ -587,7 +682,7 @@ def print_photo_info(photos, json=False):
p.uuid, p.uuid,
p.filename, p.filename,
p.original_filename, p.original_filename,
str(p.date), p.date.isoformat(),
p.description, p.description,
p.title, p.title,
", ".join(p.keywords), ", ".join(p.keywords),
@@ -603,6 +698,9 @@ def print_photo_info(photos, json=False):
p._latitude, p._latitude,
p._longitude, p._longitude,
p.path_edited, p.path_edited,
p.isphoto,
p.ismovie,
p.uti,
] ]
) )
for row in dump: for row in dump:
@@ -631,6 +729,9 @@ def _query(
not_missing, not_missing,
shared, shared,
not_shared, not_shared,
isphoto,
ismovie,
uti,
): ):
""" run a query against PhotosDB to extract the photos based on user supply criteria """ """ run a query against PhotosDB to extract the photos based on user supply criteria """
""" used by query and export commands """ """ used by query and export commands """
@@ -638,7 +739,14 @@ def _query(
""" if either is modified, need to ensure all three functions are updated """ """ if either is modified, need to ensure all three functions are updated """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db) photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
photos = photosdb.photos(keywords=keyword, persons=person, albums=album, uuid=uuid, movies=True) photos = photosdb.photos(
keywords=keyword,
persons=person,
albums=album,
uuid=uuid,
images=isphoto,
movies=ismovie,
)
if title: if title:
# search title field for text # search title field for text
@@ -656,7 +764,7 @@ def _query(
if description: if description:
# search description field for text # search description field for text
# if more than one, find photos with all name values in in description # if more than one, find photos with all name values in description
if ignore_case: if ignore_case:
# case-insensitive # case-insensitive
for d in description: for d in description:
@@ -696,6 +804,14 @@ def _query(
elif not_shared: elif not_shared:
photos = [p for p in photos if not p.shared] photos = [p for p in photos if not p.shared]
if shared:
photos = [p for p in photos if p.shared]
elif not_shared:
photos = [p for p in photos if not p.shared]
if uti:
photos = [p for p in photos if uti in p.uti]
return photos return photos

View File

@@ -1,3 +1,3 @@
""" version info """ """ version info """
__version__ = "0.19.00" __version__ = "0.19.01"

View File

@@ -536,6 +536,9 @@ class PhotoInfo:
"longitude": self._longitude, "longitude": self._longitude,
"path_edited": self.path_edited, "path_edited": self.path_edited,
"shared": self.shared, "shared": self.shared,
"isphoto": self.isphoto,
"ismovie": self.ismovie,
"uti": self.uti,
} }
return yaml.dump(info, sort_keys=False) return yaml.dump(info, sort_keys=False)
@@ -561,6 +564,9 @@ class PhotoInfo:
"longitude": self._longitude, "longitude": self._longitude,
"path_edited": self.path_edited, "path_edited": self.path_edited,
"shared": self.shared, "shared": self.shared,
"isphoto": self.isphoto,
"ismovie": self.ismovie,
"uti": self.uti,
} }
return json.dumps(pic) return json.dumps(pic)

View File

@@ -3,23 +3,23 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BackgroundHighlightCollection</key> <key>BackgroundHighlightCollection</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T09:24:37Z</date>
<key>BackgroundHighlightEnrichment</key> <key>BackgroundHighlightEnrichment</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T09:24:37Z</date>
<key>BackgroundJobAssetRevGeocode</key> <key>BackgroundJobAssetRevGeocode</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T10:47:44Z</date>
<key>BackgroundJobSearch</key> <key>BackgroundJobSearch</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T09:24:37Z</date>
<key>BackgroundPeopleSuggestion</key> <key>BackgroundPeopleSuggestion</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T09:24:36Z</date>
<key>BackgroundUserBehaviorProcessor</key> <key>BackgroundUserBehaviorProcessor</key>
<date>2019-12-28T23:29:58Z</date> <date>2019-12-28T23:29:58Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key> <key>PhotoAnalysisGraphLastBackgroundGraphConsistencyUpdateJobDateKey</key>
<date>2019-12-29T06:18:45Z</date> <date>2019-12-29T10:47:45Z</date>
<key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key> <key>PhotoAnalysisGraphLastBackgroundGraphRebuildJobDate</key>
<date>2019-12-28T23:29:57Z</date> <date>2019-12-28T23:29:57Z</date>
<key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key> <key>PhotoAnalysisGraphLastBackgroundMemoryGenerationJobDate</key>
<date>2019-12-29T06:18:40Z</date> <date>2019-12-29T09:24:37Z</date>
<key>SiriPortraitDonation</key> <key>SiriPortraitDonation</key>
<date>2019-12-28T23:29:58Z</date> <date>2019-12-28T23:29:58Z</date>
</dict> </dict>