diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 7639dbc0..8e4c9879 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -64,51 +64,137 @@ class CLI_Obj: CTX_SETTINGS = dict(help_option_names=["-h", "--help"]) +DB_OPTION = click.option( + "--db", + required=False, + metavar="", + default=None, + help=( + "Specify Photos database path. " + "Path to Photos library/database can be specified using either --db " + "or directly as PHOTOS_LIBRARY positional argument." + ), + type=click.Path(exists=True), +) + +DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) + +JSON_OPTION = click.option( + "--json", "json_", + required=False, + is_flag=True, + default=False, + help="Print output in JSON format.", +) + + +def query_options(f): + o = click.option + options = [ + o("--keyword", default=None, multiple=True, help="Search for keyword(s)."), + o("--person", default=None, multiple=True, help="Search for person(s)."), + o("--album", default=None, multiple=True, help="Search for album(s)."), + o("--uuid", default=None, multiple=True, help="Search for UUID(s)."), + o( + "--title", + default=None, + multiple=True, + help="Search for TEXT in title of photo.", + ), + o("--no-title", is_flag=True, help="Search for photos with no title."), + o( + "--description", + default=None, + multiple=True, + help="Search for TEXT in description of photo.", + ), + o( + "--no-description", + is_flag=True, + help="Search for photos with no description.", + ), + o( + "--uti", + default=None, + multiple=False, + help="Search for photos whose uniform type identifier (UTI) matches TEXT", + ), + o( + "-i", + "--ignore-case", + is_flag=True, + help="Case insensitive search for title or description. Does not apply to keyword, person, or album.", + ), + o("--edited", is_flag=True, help="Search for photos that have been edited."), + o( + "--external-edit", + is_flag=True, + help="Search for photos edited in external editor.", + ), + o("--favorite", is_flag=True, help="Search for photos marked favorite."), + o( + "--not-favorite", + is_flag=True, + help="Search for photos not marked favorite.", + ), + o("--hidden", is_flag=True, help="Search for photos marked hidden."), + o("--not-hidden", is_flag=True, help="Search for photos not marked hidden."), + o( + "--shared", + is_flag=True, + help="Search for photos in shared iCloud album (Photos 5 only).", + ), + o( + "--not-shared", + is_flag=True, + help="Search for photos not in shared iCloud album (Photos 5 only).", + ), + o( + "--burst", + is_flag=True, + help="Search for photos that were taken in a burst.", + ), + o( + "--not-burst", + is_flag=True, + help="Search for photos that are not part of a burst.", + ), + o("--live", is_flag=True, help="Search for Apple live photos"), + o( + "--not-live", + is_flag=True, + help="Search for photos that are not Apple live photos", + ), + o( + "--only-movies", + is_flag=True, + help="Search only for movies (default searches both images and movies).", + ), + o( + "--only-photos", + is_flag=True, + help="Search only for photos/images (default searches both images and movies).", + ), + ] + for o in options[::-1]: + f = o(f) + return f @click.group(context_settings=CTX_SETTINGS) -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path.", - type=click.Path(exists=True), -) -@click.option( - "--json", - required=False, - is_flag=True, - default=False, - help="Print output in JSON format.", -) +@DB_OPTION +@JSON_OPTION @click.option("--debug", required=False, is_flag=True, default=False, hidden=True) @click.version_option(__version__, "--version", "-v") @click.pass_context -def cli(ctx, db, json, debug): - ctx.obj = CLI_Obj(db=db, json=json, debug=debug) +def cli(ctx, db, json_, debug): + ctx.obj = CLI_Obj(db=db, json=json_, debug=debug) @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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)) +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT @click.pass_obj @click.pass_context def keywords(ctx, cli_obj, db, json_, photos_library): @@ -116,8 +202,8 @@ def keywords(ctx, cli_obj, db, json_, photos_library): 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: ") + click.echo(cli.commands["keywords"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -130,25 +216,9 @@ def keywords(ctx, cli_obj, db, json_, photos_library): @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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)) +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT @click.pass_obj @click.pass_context def albums(ctx, cli_obj, db, json_, photos_library): @@ -156,8 +226,8 @@ def albums(ctx, cli_obj, db, json_, photos_library): 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: ") + click.echo(cli.commands["albums"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -173,25 +243,9 @@ def albums(ctx, cli_obj, db, json_, photos_library): @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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)) +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT @click.pass_obj @click.pass_context def persons(ctx, cli_obj, db, json_, photos_library): @@ -199,8 +253,8 @@ def persons(ctx, cli_obj, db, json_, photos_library): 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: ") + click.echo(cli.commands["persons"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -213,25 +267,9 @@ def persons(ctx, cli_obj, db, json_, photos_library): @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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)) +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT @click.pass_obj @click.pass_context def info(ctx, cli_obj, db, json_, photos_library): @@ -239,8 +277,8 @@ def info(ctx, cli_obj, db, json_, photos_library): 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: ") + click.echo(cli.commands["info"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -302,25 +340,9 @@ def info(ctx, cli_obj, db, json_, photos_library): @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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)) +@DB_OPTION +@JSON_OPTION +@DB_ARGUMENT @click.pass_obj @click.pass_context def dump(ctx, cli_obj, db, json_, photos_library): @@ -328,8 +350,8 @@ def dump(ctx, cli_obj, db, json_, 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: ") + click.echo(cli.commands["dump"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -339,22 +361,15 @@ def dump(ctx, cli_obj, db, json_, photos_library): @cli.command(name="list") -@click.option( - "--json", - "json_", - required=False, - is_flag=True, - default=False, - help="Print output in JSON format.", -) +@JSON_OPTION @click.pass_obj @click.pass_context def list_libraries(ctx, cli_obj, json_): """ Print list of Photos libraries found on the system. """ - _list_libraries(json_=json_ or cli_obj.json) + _list_libraries(json_=json_ or cli_obj.json, error=False) -def _list_libraries(json_=False): +def _list_libraries(json_=False, error=True): """ Print list of Photos libraries found on the system. If json_ == True, print output as JSON (default = False) """ @@ -374,106 +389,32 @@ def _list_libraries(json_=False): for lib in photo_libs: if lib == sys_lib: - click.echo(f"(*)\t{lib}") + click.echo(f"(*)\t{lib}", err=error) sys_lib_flag = True elif lib == last_lib: - click.echo(f"(#)\t{lib}") + click.echo(f"(#)\t{lib}", err=error) last_lib_flag = True else: - click.echo(f"\t{lib}") + click.echo(f"\t{lib}", err=error) if sys_lib_flag or last_lib_flag: - click.echo("\n") + click.echo("\n", err=error) if sys_lib_flag: - click.echo("(*)\tSystem Photos Library") + click.echo("(*)\tSystem Photos Library", err=error) if last_lib_flag: - click.echo("(#)\tLast opened Photos Library") + click.echo("(#)\tLast opened Photos Library", err=error) @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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).") -@click.option("--uuid", default=None, multiple=True, help="Search for UUID(s).") -@click.option( - "--title", default=None, multiple=True, help="Search for TEXT in title of photo." -) -@click.option("--no-title", is_flag=True, help="Search for photos with no title.") -@click.option( - "--description", - default=None, - multiple=True, - help="Search for TEXT in description of photo.", -) -@click.option( - "--no-description", is_flag=True, help="Search for photos with no description." -) -@click.option( - "--uti", - default=None, - multiple=False, - help="Search for photos whose uniform type identifier (UTI) matches TEXT", -) -@click.option( - "-i", - "--ignore-case", - is_flag=True, - help="Case insensitive search for title 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( - "--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( - "--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.") +@DB_OPTION +@JSON_OPTION +@query_options @click.option("--missing", is_flag=True, help="Search for photos missing from disk.") @click.option( "--not-missing", is_flag=True, help="Search for photos present on disk (e.g. not missing).", ) -@click.option( - "--shared", - is_flag=True, - help="Search for photos in shared iCloud album (Photos 5 only).", -) -@click.option( - "--not-shared", - is_flag=True, - help="Search for photos not in shared iCloud album (Photos 5 only).", -) -@click.option( - "--burst", is_flag=True, help="Search for photos that were taken in a burst." -) -@click.option( - "--not-burst", is_flag=True, help="Search for photos that are not part of a burst." -) -@click.option("--live", is_flag=True, help="Search for Apple live photos") -@click.option( - "--not-live", is_flag=True, help="Search for photos that are not Apple live photos" -) @click.option( "--cloudasset", is_flag=True, @@ -494,17 +435,7 @@ def _list_libraries(json_=False): is_flag=True, help="Search for photos that are not in iCloud (have not been synched)", ) -@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("photos_library", nargs=-1, type=click.Path(exists=True)) +@DB_ARGUMENT @click.pass_obj @click.pass_context def query( @@ -635,8 +566,8 @@ def query( 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: ") + click.echo(cli.commands["query"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -677,75 +608,8 @@ def query( @cli.command() -@click.option( - "--db", - required=False, - metavar="", - default=None, - help="Specify Photos database path. " - "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument.", - 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).") -@click.option("--uuid", default=None, multiple=True, help="Search for UUID(s).") -@click.option( - "--title", default=None, multiple=True, help="Search for TEXT in title of photo." -) -@click.option("--no-title", is_flag=True, help="Search for photos with no title.") -@click.option( - "--description", - default=None, - multiple=True, - help="Search for TEXT in description of photo.", -) -@click.option( - "--no-description", is_flag=True, help="Search for photos with no description." -) -@click.option( - "--uti", - default=None, - multiple=False, - help="Search for photos whose uniform type identifier (UTI) matches TEXT", -) -@click.option( - "-i", - "--ignore-case", - is_flag=True, - help="Case insensitive search for title 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( - "--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( - "--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( - "--burst", is_flag=True, help="Search for photos that were taken in a burst." -) -@click.option( - "--not-burst", is_flag=True, help="Search for photos that are not part of a burst." -) -@click.option("--live", is_flag=True, help="Search for Apple live photos") -@click.option( - "--not-live", is_flag=True, help="Search for photos that are not Apple live photos" -) -@click.option( - "--shared", - is_flag=True, - help="Search for photos in shared iCloud album (Photos 5 only).", -) -@click.option( - "--not-shared", - is_flag=True, - help="Search for photos not in shared iCloud album (Photos 5 only).", -) +@DB_OPTION +@query_options @click.option("--verbose", "-V", is_flag=True, help="Print verbose output.") @click.option( "--overwrite", @@ -793,16 +657,6 @@ def query( "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.", ) -@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( "--download-missing", is_flag=True, @@ -811,7 +665,7 @@ 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("photos_library", nargs=-1, type=click.Path(exists=True)) +@DB_ARGUMENT @click.argument("dest", nargs=1, type=click.Path(exists=True)) @click.pass_obj @click.pass_context @@ -904,8 +758,8 @@ def export( 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: ") + click.echo(cli.commands["export"].get_help(ctx), err=True) + click.echo("\n\nLocated the following Photos library databases: ", err=True) _list_libraries() return @@ -1000,7 +854,7 @@ def help(ctx, topic, **kw): if topic is None: click.echo(ctx.parent.get_help()) else: - ctx.info_name = topic + ctx.info_name = topic click.echo(cli.commands[topic].get_help(ctx))