Added list option to cmd_line. Closes #14

This commit is contained in:
Rhet Turnbull
2019-12-08 09:14:48 -08:00
parent 7fef67f852
commit aaba5cabf3
4 changed files with 125 additions and 78 deletions

View File

@@ -37,19 +37,20 @@ After installing pipx:
Usage: osxphotos [OPTIONS] COMMAND [ARGS]... Usage: osxphotos [OPTIONS] COMMAND [ARGS]...
Options: Options:
--db <Photos database path> Specify database file --db <Photos database path> Specify database file.
--json Print output in JSON format --json Print output in JSON format.
-v, --version Show the version and exit. -v, --version Show the version and exit.
-h, --help Show this message and exit. -h, --help Show this message and exit.
Commands: Commands:
albums print out albums found in the Photos library albums Print out albums found in the Photos library.
dump print list of all photos & associated info from the Photos... dump Print list of all photos & associated info from the Photos...
help print help; for help on commands: help <command> help Print help; for help on commands: help <command>.
info print out descriptive info of the Photos library database info Print out descriptive info of the Photos library database.
keywords print out keywords found in the Photos library keywords Print out keywords found in the Photos library.
persons print out persons (faces) found in the Photos library list Print list of Photos libraries found on the system.
query query the Photos database using 1 or more search options; if... persons Print out persons (faces) found in the Photos library.
query Query the Photos database using 1 or more search options; if...
``` ```
To get help on a specific command, use `osxphotos help <command_name>` To get help on a specific command, use `osxphotos help <command_name>`
@@ -59,27 +60,29 @@ Example: `osxphotos help query`
``` ```
Usage: osxphotos help [OPTIONS] Usage: osxphotos help [OPTIONS]
query the Photos database using 1 or more search options; if more than 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 one option is provided, they are treated as "AND" (e.g. search for photos
matching all options) matching all options).
Options: Options:
--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).
--uuid TEXT search for UUID(s) --uuid TEXT Search for UUID(s).
--name TEXT search for TEXT in name of photo --name TEXT Search for TEXT in name of photo.
--no-name search for photos with no name --no-name Search for photos with no name.
--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.
-i, --ignore-case case insensitive search for name or description. Does -i, --ignore-case Case insensitive search for name or description. Does
not apply to keyword, person, or album not apply to keyword, person, or album.
--favorite search for photos marked favorite --edited Search for photos that have been edited.
--not-favorite search for photos not marked favorite --external-edit Search for photos edited in external editor.
--hidden search for photos marked hidden --favorite Search for photos marked favorite.
--not-hidden search for photos not marked hidden --not-favorite Search for photos not marked favorite.
--missing search for photos missing from disk --hidden Search for photos marked hidden.
--not-missing search for photos present on disk (e.g. not missing) --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 --json Print output in JSON format
-h, --help Show this message and exit. -h, --help Show this message and exit.
``` ```
@@ -141,7 +144,7 @@ Returns path to last opened Photo Library as string.
#### ```list_photo_libraries()``` #### ```list_photo_libraries()```
Returns list of Photos libraries found on the system Returns list of Photos libraries found on the system. **Note**: On MacOS 10.15, this appears to list all libraries. On older systems, it may not find some libraries if they are not located in ~/Pictures. Provided for convenience but do not rely on this to find all libraries on the system.
### PhotosDB ### PhotosDB

View File

@@ -1,9 +1,10 @@
import glob
import json import json
import logging import logging
import os.path import os.path
import platform import platform
import subprocess
import sqlite3 import sqlite3
import subprocess
import sys import sys
import tempfile import tempfile
import urllib.parse import urllib.parse
@@ -192,15 +193,25 @@ def get_last_library_path():
def list_photo_libraries(): def list_photo_libraries():
""" returns list of Photos libraries found on the system """ """ returns list of Photos libraries found on the system """
# lib_list = glob.glob(f"{str(Path.home())}/Pictures/*.photoslibrary") """ on MacOS < 10.15, this may omit some libraries """
# return lib_list
# On 10.15, mdfind appears to find all libraries
# On older MacOS versions, mdfind appears to ignore some libraries
# glob to find libraries in ~/Pictures then mdfind to find all the others
lib_list = glob.glob(f"{str(Path.home())}/Pictures/*.photoslibrary")
# On older OS, may not get all libraries so make sure we get the last one
last_lib = get_last_library_path()
if last_lib:
lib_list.append(last_lib)
lib_list = []
output = subprocess.check_output( output = subprocess.check_output(
["/usr/bin/mdfind", "-onlyin", "/", "-name", ".photoslibrary"] ["/usr/bin/mdfind", "-onlyin", "/", "-name", ".photoslibrary"]
).splitlines() ).splitlines()
for lib in output: for lib in output:
lib_list.append(lib.decode("utf-8")) lib_list.append(lib.decode("utf-8"))
lib_list = list(set(lib_list))
lib_list.sort()
return lib_list return lib_list

View File

@@ -1,4 +1,4 @@
""" version info """ """ version info """
__version__ = "0.14.19" __version__ = "0.14.20"

View File

@@ -29,14 +29,14 @@ 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 database file.",
) )
@click.option( @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.option("--debug", required=False, is_flag=True, default=False, hidden=True) @click.option("--debug", required=False, is_flag=True, default=False, hidden=True)
@click.version_option(__version__, "--version", "-v") @click.version_option(__version__, "--version", "-v")
@@ -48,43 +48,43 @@ def cli(ctx, db, json, debug):
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def keywords(cli_obj): def keywords(cli_obj):
""" print out keywords found in the Photos library""" """ Print out keywords found in the Photos library. """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db) photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
keywords = {"keywords": photosdb.keywords_as_dict()} keywords = {"keywords": photosdb.keywords_as_dict()}
if cli_obj.json: if cli_obj.json:
print(json.dumps(keywords)) click.echo(json.dumps(keywords))
else: else:
print(yaml.dump(keywords, sort_keys=False)) click.echo(yaml.dump(keywords, sort_keys=False))
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def albums(cli_obj): def albums(cli_obj):
""" print out albums found in the Photos library """ """ Print out albums found in the Photos library. """
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db) photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
albums = {"albums": photosdb.albums_as_dict()} albums = {"albums": photosdb.albums_as_dict()}
if cli_obj.json: if cli_obj.json:
print(json.dumps(albums)) click.echo(json.dumps(albums))
else: else:
print(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):
""" 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) photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
persons = {"persons": photosdb.persons_as_dict()} persons = {"persons": photosdb.persons_as_dict()}
if cli_obj.json: if cli_obj.json:
print(json.dumps(persons)) click.echo(json.dumps(persons))
else: else:
print(yaml.dump(persons, sort_keys=False)) click.echo(yaml.dump(persons, sort_keys=False))
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def info(cli_obj): def info(cli_obj):
""" 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) pdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
info = {} info = {}
info["database_path"] = pdb.get_db_path() info["database_path"] = pdb.get_db_path()
@@ -117,57 +117,90 @@ def info(cli_obj):
info["persons"] = persons info["persons"] = persons
if cli_obj.json: if cli_obj.json:
print(json.dumps(info)) click.echo(json.dumps(info))
else: else:
print(yaml.dump(info, sort_keys=False)) click.echo(yaml.dump(info, sort_keys=False))
@cli.command() @cli.command()
@click.pass_obj @click.pass_obj
def dump(cli_obj): def dump(cli_obj):
""" print list of all photos & associated info from the Photos library """ """ Print list of all photos & associated info from the Photos library. """
pdb = osxphotos.PhotosDB(dbfile=cli_obj.db) pdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
photos = pdb.photos() photos = pdb.photos()
print_photo_info(photos, cli_obj.json) print_photo_info(photos, cli_obj.json)
@cli.command(name="list")
@click.pass_obj
def list_libraries(cli_obj):
""" Print list of Photos libraries found on the system. """
photo_libs = osxphotos.list_photo_libraries()
sys_lib = None
_, major, _ = osxphotos._get_os_version()
if int(major) >= 15:
sys_lib = osxphotos.get_system_library_path()
last_lib = osxphotos.get_last_library_path()
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}")
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() @cli.command()
@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).")
@click.option("--uuid", default=None, multiple=True, help="search for UUID(s)") @click.option("--uuid", default=None, multiple=True, help="Search for UUID(s).")
@click.option( @click.option(
"--name", default=None, multiple=True, help="search for TEXT in name of photo" "--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("--no-name", is_flag=True, help="Search for photos with no name.")
@click.option( @click.option(
"--description", "--description",
default=None, default=None,
multiple=True, multiple=True,
help="search for TEXT in description of photo", help="Search for TEXT in description of photo.",
) )
@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( @click.option(
"-i", "-i",
"--ignore-case", "--ignore-case",
is_flag=True, is_flag=True,
help="case insensitive search for name or description. Does not apply to keyword, person, or album", help="Case insensitive search for name or description. Does not apply to keyword, person, or album.",
) )
@click.option("--edited", is_flag=True, help="search for photos that have been edited") @click.option("--edited", is_flag=True, help="Search for photos that have been edited.")
@click.option("--external-edit", is_flag=True, help="search for photos edited in external editor")
@click.option("--favorite", is_flag=True, help="search for photos marked favorite")
@click.option( @click.option(
"--not-favorite", is_flag=True, help="search for photos not marked favorite" "--external-edit", is_flag=True, help="Search for photos edited in external editor."
) )
@click.option("--hidden", is_flag=True, help="search for photos marked hidden") @click.option("--favorite", is_flag=True, help="Search for photos marked favorite.")
@click.option("--not-hidden", is_flag=True, help="search for photos not marked hidden") @click.option(
@click.option("--missing", is_flag=True, help="search for photos missing from disk") "--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( @click.option(
"--not-missing", "--not-missing",
is_flag=True, is_flag=True,
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(
"--json", "--json",
@@ -200,9 +233,9 @@ def query(
missing, missing,
not_missing, not_missing,
): ):
""" 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"
(e.g. search for photos matching all options) (e.g. search for photos matching all options).
""" """
# if no query terms, show help and return # if no query terms, show help and return
@@ -226,27 +259,27 @@ def query(
not_missing, not_missing,
] ]
): ):
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
elif favorite and not_favorite: elif favorite and not_favorite:
# can't search for both favorite and notfavorite # can't search for both favorite and notfavorite
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
elif hidden and not_hidden: elif hidden and not_hidden:
# can't search for both hidden and nothidden # can't search for both hidden and nothidden
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
elif missing and not_missing: elif missing and not_missing:
# can't search for both missing and notmissing # can't search for both missing and notmissing
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
elif name and no_name: elif name and no_name:
# can't search for both name and no_name # can't search for both name and no_name
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
elif description and no_description: elif description and no_description:
# can't search for both description and no_description # can't search for both description and no_description
print(cli.commands["query"].get_help(ctx)) click.echo(cli.commands["query"].get_help(ctx))
return return
else: else:
photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db) photosdb = osxphotos.PhotosDB(dbfile=cli_obj.db)
@@ -316,11 +349,11 @@ def query(
@click.argument("topic", default=None, required=False, nargs=1) @click.argument("topic", default=None, required=False, nargs=1)
@click.pass_context @click.pass_context
def help(ctx, topic, **kw): def help(ctx, topic, **kw):
""" print help; for help on commands: help <command> """ """ Print help; for help on commands: help <command>. """
if topic is None: if topic is None:
print(ctx.parent.get_help()) click.echo(ctx.parent.get_help())
else: else:
print(cli.commands[topic].get_help(ctx)) click.echo(cli.commands[topic].get_help(ctx))
def print_photo_info(photos, json=False): def print_photo_info(photos, json=False):
@@ -328,7 +361,7 @@ def print_photo_info(photos, json=False):
dump = [] dump = []
for p in photos: for p in photos:
dump.append(p.to_json()) dump.append(p.to_json())
print(f"[{', '.join(dump)}]") click.echo(f"[{', '.join(dump)}]")
else: else:
# dump as CSV # dump as CSV
csv_writer = csv.writer( csv_writer = csv.writer(