diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 7639dbc0..051cb0a1 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -64,24 +64,126 @@ class CLI_Obj: CTX_SETTINGS = dict(help_option_names=["-h", "--help"]) - - -@click.group(context_settings=CTX_SETTINGS) -@click.option( +DB_OPTION = click.option( "--db", required=False, metavar="", default=None, - help="Specify Photos database path.", + 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( + +DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) + +JSON_OPTION = click.option( "--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) +@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 @@ -90,25 +192,9 @@ def cli(ctx, db, json, 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): @@ -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): @@ -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): @@ -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): @@ -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): @@ -339,14 +361,7 @@ 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_): @@ -391,89 +406,15 @@ def _list_libraries(json_=False): @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( @@ -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