From e214746063271e6f9f586286103ed051ada49d85 Mon Sep 17 00:00:00 2001 From: mwort Date: Mon, 20 Jan 2020 00:55:43 +0100 Subject: [PATCH 01/15] Refactor cli: singular --db, --json and query options. --- osxphotos/__main__.py | 408 ++++++++++++++---------------------------- 1 file changed, 131 insertions(+), 277 deletions(-) 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 From 6e364fc9d987fefe3a751e0ab40d63bc7dbe9409 Mon Sep 17 00:00:00 2001 From: mwort Date: Mon, 20 Jan 2020 00:56:23 +0100 Subject: [PATCH 02/15] Write out list of libraries to stderr except in list command. --- osxphotos/__main__.py | 46 +++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 051cb0a1..00e50963 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -202,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 @@ -226,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 @@ -253,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 @@ -277,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 @@ -350,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 @@ -366,10 +366,10 @@ def dump(ctx, cli_obj, db, json_, photos_library): @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) """ @@ -389,20 +389,20 @@ 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() @@ -566,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 @@ -758,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 @@ -854,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)) From a2067709ccb1648454e732421fbf436a73e1d842 Mon Sep 17 00:00:00 2001 From: mwort Date: Mon, 20 Jan 2020 01:09:55 +0100 Subject: [PATCH 03/15] Fix json_ argument to cli. --- osxphotos/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 00e50963..8e4c9879 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -80,7 +80,7 @@ DB_OPTION = click.option( DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) JSON_OPTION = click.option( - "--json", + "--json", "json_", required=False, is_flag=True, default=False, @@ -187,8 +187,8 @@ def query_options(f): @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() From b544e2f17156d374af35c436ca17cefa66420df2 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 19 Jan 2020 18:08:21 -0800 Subject: [PATCH 04/15] version bump --- osxphotos/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 4e9dee0e..32d7837e 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.22.1" +__version__ = "0.22.2" From b9dee4995c6d89fadb3d2482374b7098f2ab5ed9 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 19 Jan 2020 19:50:34 -0800 Subject: [PATCH 05/15] Refactored _query. Still hairy, but less so. --- osxphotos/__main__.py | 191 +++++++++++++++++++++--------------------- osxphotos/_version.py | 2 +- 2 files changed, 96 insertions(+), 97 deletions(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 8e4c9879..f5d989a7 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -80,7 +80,8 @@ DB_OPTION = click.option( DB_ARGUMENT = click.argument("photos_library", nargs=-1, type=click.Path(exists=True)) JSON_OPTION = click.option( - "--json", "json_", + "--json", + "json_", required=False, is_flag=True, default=False, @@ -572,37 +573,37 @@ def query( return photos = _query( - db, - keyword, - person, - album, - uuid, - title, - no_title, - description, - no_description, - ignore_case, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - missing, - not_missing, - shared, - not_shared, - isphoto, - ismovie, - uti, - burst, - not_burst, - live, - not_live, - cloudasset, - not_cloudasset, - incloud, - not_incloud, + db=db, + keyword=keyword, + person=person, + album=album, + uuid=uuid, + title=title, + no_title=no_title, + description=description, + no_description=no_description, + ignore_case=ignore_case, + edited=edited, + external_edit=external_edit, + favorite=favorite, + not_favorite=not_favorite, + hidden=hidden, + not_hidden=not_hidden, + missing=missing, + not_missing=not_missing, + shared=shared, + not_shared=not_shared, + isphoto=isphoto, + ismovie=ismovie, + uti=uti, + burst=burst, + not_burst=not_burst, + live=live, + not_live=not_live, + cloudasset=cloudasset, + not_cloudasset=not_cloudasset, + incloud=incloud, + not_incloud=not_incloud, ) print_photo_info(photos, cli_obj.json or json_) @@ -764,37 +765,37 @@ def export( return photos = _query( - db, - keyword, - person, - album, - uuid, - title, - no_title, - description, - no_description, - ignore_case, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - None, # missing -- won't export these but will warn user - None, # not-missing - shared, - not_shared, - isphoto, - ismovie, - uti, - burst, - not_burst, - live, - not_live, - False, # cloudasset - False, # not_cloudasset - False, # incloud - False, # not_incloud + db=db, + keyword=keyword, + person=person, + album=album, + uuid=uuid, + title=title, + no_title=no_title, + description=description, + no_description=no_description, + ignore_case=ignore_case, + edited=edited, + external_edit=external_edit, + favorite=favorite, + not_favorite=not_favorite, + hidden=hidden, + not_hidden=not_hidden, + missing=None, # missing -- won't export these but will warn user + not_missing=None, + shared=shared, + not_shared=not_shared, + isphoto=isphoto, + ismovie=ismovie, + uti=uti, + burst=burst, + not_burst=not_burst, + live=live, + not_live=not_live, + cloudasset=False, + not_cloudasset=False, + incloud=False, + not_incloud=False, ) if photos: @@ -939,45 +940,43 @@ def print_photo_info(photos, json=False): def _query( - db, - keyword, - person, - album, - uuid, - title, - no_title, - description, - no_description, - ignore_case, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - missing, - not_missing, - shared, - not_shared, - isphoto, - ismovie, - uti, - burst, - not_burst, - live, - not_live, - cloudasset, - not_cloudasset, - incloud, - not_incloud, + db=None, + keyword=None, + person=None, + album=None, + uuid=None, + title=None, + no_title=None, + description=None, + no_description=None, + ignore_case=None, + edited=None, + external_edit=None, + favorite=None, + not_favorite=None, + hidden=None, + not_hidden=None, + missing=None, + not_missing=None, + shared=None, + not_shared=None, + isphoto=None, + ismovie=None, + uti=None, + burst=None, + not_burst=None, + live=None, + not_live=None, + cloudasset=None, + not_cloudasset=None, + incloud=None, + not_incloud=None, ): """ run a query against PhotosDB to extract the photos based on user supply criteria """ """ used by query and export commands """ """ arguments must be passed in same order as query and export """ """ if either is modified, need to ensure all three functions are updated """ - # TODO: this is getting too hairy -- need to change to named args - photosdb = osxphotos.PhotosDB(dbfile=db) photos = photosdb.photos( keywords=keyword, diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 32d7837e..7ff27590 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.22.2" +__version__ = "0.22.3" From f0b18c3d29b2141d348be0495013c51c072c6251 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 19 Jan 2020 21:27:40 -0800 Subject: [PATCH 06/15] Started adding tests for CLI --- osxphotos/__main__.py | 9 ++++-- tests/test_cli.py | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tests/test_cli.py diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index f5d989a7..dd8025d7 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -565,7 +565,9 @@ def query( if only_photos: ismovie = False - db = get_photos_db(*photos_library, db, cli_obj.db) + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) if db is None: click.echo(cli.commands["query"].get_help(ctx), err=True) click.echo("\n\nLocated the following Photos library databases: ", err=True) @@ -605,7 +607,10 @@ def query( incloud=incloud, not_incloud=not_incloud, ) - print_photo_info(photos, cli_obj.json or json_) + + # below needed for to make CliRunner work for testing + cli_json = cli_obj.json if cli_obj is not None else None + print_photo_info(photos, cli_json or json_) @cli.command() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..9fb0cb74 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,70 @@ +import pytest +from click.testing import CliRunner + +CLI_OUTPUT_NO_SUBCOMMAND = [ + "Options:", + "--db Specify Photos database path. Path to Photos", + "library/database can be specified using either", + "--db or directly as PHOTOS_LIBRARY positional", + "argument.", + "--json Print output in JSON format.", + "-v, --version Show the version and exit.", + "-h, --help Show this message and exit.", + "Commands:", + " albums Print out albums found in the Photos library.", + " dump Print list of all photos & associated info from the Photos", + " export Export photos from the Photos database.", + " help Print help; for help on commands: help .", + " info Print out descriptive info of the Photos library database.", + " keywords Print out keywords found in the Photos library.", + " list Print list of Photos libraries found on the system.", + " persons Print out persons (faces) found in the Photos library.", + " query Query the Photos database using 1 or more search options; if", +] + +CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album"], "persons": ["Katie"], "path": "/tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": null, "longitude": null, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]' + + +def test_osxphotos(): + import osxphotos + from osxphotos.__main__ import cli + + runner = CliRunner() + result = runner.invoke(cli, []) + output = result.output + assert result.exit_code == 0 + for line in CLI_OUTPUT_NO_SUBCOMMAND: + assert line in output + + +def test_query_uuid(): + import json + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + result = runner.invoke( + query, + [ + "--json", + "--db", + "./tests/Test-10.15.1.photoslibrary", + "--uuid", + "D79B8D77-BFFC-460B-9312-034F2877D35B", + ], + ) + assert result.exit_code == 0 + + json_expected = json.loads(CLI_OUTPUT_QUERY_UUID)[0] + json_got = json.loads(result.output)[0] + + assert list(json_expected.keys()).sort() == list(json_got.keys()).sort() + + # check values expected vs got + # path needs special handling as path is set to full path which will differ system to system + for key_ in json_expected: + assert key_ in json_got + if key_ != "path": + assert json_expected[key_] == json_got[key_] + else: + assert json_expected[key_] in json_got[key_] From bed73780397184d37ed23208d24f01959920977f Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 19 Jan 2020 21:55:13 -0800 Subject: [PATCH 07/15] Added CLI test for export --- osxphotos/__main__.py | 5 ++++- tests/test_cli.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index dd8025d7..44d7e7fd 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -762,7 +762,10 @@ def export( if only_photos: ismovie = False - db = get_photos_db(*photos_library, db, cli_obj.db) + + # below needed for to make CliRunner work for testing + cli_db = cli_obj.db if cli_obj is not None else None + db = get_photos_db(*photos_library, db, cli_db) if db is None: click.echo(cli.commands["export"].get_help(ctx), err=True) click.echo("\n\nLocated the following Photos library databases: ", err=True) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9fb0cb74..c3e21ea7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -24,6 +24,17 @@ CLI_OUTPUT_NO_SUBCOMMAND = [ CLI_OUTPUT_QUERY_UUID = '[{"uuid": "D79B8D77-BFFC-460B-9312-034F2877D35B", "filename": "D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "original_filename": "Pumkins2.jpg", "date": "2018-09-28T16:07:07-04:00", "description": "Girl holding pumpkin", "title": "I found one!", "keywords": ["Kids"], "albums": ["Pumpkin Farm", "Test Album"], "persons": ["Katie"], "path": "/tests/Test-10.15.1.photoslibrary/originals/D/D79B8D77-BFFC-460B-9312-034F2877D35B.jpeg", "ismissing": false, "hasadjustments": false, "external_edit": false, "favorite": false, "hidden": false, "latitude": null, "longitude": null, "path_edited": null, "shared": false, "isphoto": true, "ismovie": false, "uti": "public.jpeg", "burst": false, "live_photo": false, "path_live_photo": null, "iscloudasset": false, "incloud": null}]' +CLI_EXPORT_FILENAMES = [ + "Pumkins1.jpg", + "Pumkins2.jpg", + "Pumpkins3.jpg", + "St James Park.jpg", + "St James Park_edited.jpg", + "Tulips.jpg", + "wedding.jpg", + "wedding_edited.jpg", +] + def test_osxphotos(): import osxphotos @@ -68,3 +79,27 @@ def test_query_uuid(): assert json_expected[key_] == json_got[key_] else: assert json_expected[key_] in json_got[key_] + + +def test_export(): + import glob + import os + import os.path + import osxphotos + from osxphotos.__main__ import export + + runner = CliRunner() + cwd = os.getcwd() + with runner.isolated_filesystem(): + result = runner.invoke( + export, + [ + os.path.join(cwd, "tests/Test-10.15.1.photoslibrary"), + ".", + "--original-name", + "--export-edited", + "-V", + ], + ) + files = glob.glob("*.jpg") + assert files.sort() == CLI_EXPORT_FILENAMES.sort() From f1e872401c25b858dc698a5d9c1d4696c48e22ba Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Sun, 19 Jan 2020 22:21:46 -0800 Subject: [PATCH 08/15] Cleaned up comments --- osxphotos/__main__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 44d7e7fd..d5999dbf 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -15,9 +15,6 @@ from ._constants import _EXIF_TOOL_URL, _PHOTOS_5_VERSION from ._version import __version__ from .utils import create_path_by_date, _copy_file -# TODO: add "--any" to search any field (e.g. keyword, description, title contains "wedding") (add case insensitive option) -# TODO: add search for filename - def get_photos_db(*db_options): """ Return path to photos db, select first non-None arg @@ -367,6 +364,9 @@ def dump(ctx, cli_obj, db, json_, photos_library): @click.pass_context def list_libraries(ctx, cli_obj, json_): """ Print list of Photos libraries found on the system. """ + + # implemented in _list_libraries so it can be called by other CLI functions + # without errors due to passing ctx and cli_obj _list_libraries(json_=json_ or cli_obj.json, error=False) @@ -762,7 +762,6 @@ def export( if only_photos: ismovie = False - # below needed for to make CliRunner work for testing cli_db = cli_obj.db if cli_obj is not None else None db = get_photos_db(*photos_library, db, cli_db) From cfa2b4a828facf0aff5bc19f777457ad776c4a05 Mon Sep 17 00:00:00 2001 From: mwort Date: Mon, 20 Jan 2020 14:04:50 +0100 Subject: [PATCH 09/15] Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. --- osxphotos/__main__.py | 161 ++++++++++++--------------------- osxphotos/photosdb.py | 17 +++- tests/test_catalina_10_15_1.py | 17 ++++ tests/test_cli.py | 22 +++++ 4 files changed, 115 insertions(+), 102 deletions(-) diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index d5999dbf..74333e9d 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -173,6 +173,16 @@ def query_options(f): is_flag=True, help="Search only for photos/images (default searches both images and movies).", ), + o( + "--from-date", + help="Search by start item date, e.g. 2000-01-12T12:00:00 or 2000-12-31 (ISO 8601 w/o TZ).", + type=click.DateTime(), + ), + o( + "--to-date", + help="Search by end item date, e.g. 2000-01-12T12:00:00 or 2000-12-31 (ISO 8601 w/o TZ).", + type=click.DateTime(), + ), ] for o in options[::-1]: f = o(f) @@ -475,6 +485,8 @@ def query( not_cloudasset, incloud, not_incloud, + from_date, + to_date, ): """ Query the Photos database using 1 or more search options; if more than one option is provided, they are treated as "AND" @@ -482,80 +494,33 @@ def query( """ # if no query terms, show help and return - if not any( - [ - keyword, - person, - album, - uuid, - title, - no_title, - description, - no_description, - edited, - external_edit, - favorite, - not_favorite, - hidden, - not_hidden, - missing, - not_missing, - shared, - not_shared, - only_movies, - only_photos, - uti, - burst, - not_burst, - live, - not_live, - cloudasset, - not_cloudasset, - incloud, - not_incloud, - ] - ): - click.echo(cli.commands["query"].get_help(ctx)) - return - elif favorite and not_favorite: - # can't search for both favorite and notfavorite - click.echo(cli.commands["query"].get_help(ctx)) - return - elif hidden and not_hidden: - # can't search for both hidden and nothidden - click.echo(cli.commands["query"].get_help(ctx)) - return - elif missing and not_missing: - # can't search for both missing and notmissing - click.echo(cli.commands["query"].get_help(ctx)) - return - elif title and no_title: - # can't search for both title and no_title - click.echo(cli.commands["query"].get_help(ctx)) - return - elif description and no_description: - # can't search for both description and no_description - click.echo(cli.commands["query"].get_help(ctx)) - return - elif only_photos and only_movies: - # can't have only photos and only movies - click.echo(cli.commands["query"].get_help(ctx)) - return - elif burst and not_burst: - # can't search for both burst and not_burst - click.echo(cli.commands["query"].get_help(ctx)) - return - elif live and not_live: - # can't search for both live and not_live - click.echo(cli.commands["query"].get_help(ctx)) - return - elif cloudasset and not_cloudasset: - # can't search for both live and not_live - click.echo(cli.commands["query"].get_help(ctx)) - return - elif incloud and not_incloud: - # can't search for both live and not_live - click.echo(cli.commands["query"].get_help(ctx)) + # sanity check input args + nonexclusive = [ + keyword, + person, + album, + uuid, + edited, + external_edit, + uti, + from_date, + to_date, + ] + exclusive = [ + (favorite, not_favorite), + (hidden, not_hidden), + (missing, not_missing), + (any(title), no_title), + (any(description), no_description), + (only_photos, only_movies), + (burst, not_burst), + (live, not_live), + (cloudasset, not_cloudasset), + (incloud, not_incloud), + ] + # print help if no non-exclusive term or a double exclusive term is given + if not any(nonexclusive + [b ^ n for b, n in exclusive]): + click.echo(cli.commands["query"].get_help(ctx), err=True) return # actually have something to query @@ -606,6 +571,8 @@ def query( not_cloudasset=not_cloudasset, incloud=incloud, not_incloud=not_incloud, + from_date=from_date, + to_date=to_date, ) # below needed for to make CliRunner work for testing @@ -698,6 +665,8 @@ def export( not_hidden, shared, not_shared, + from_date, + to_date, verbose, overwrite, export_by_date, @@ -727,33 +696,17 @@ def export( sys.exit("DEST must be valid path") # 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 - elif burst and not_burst: - # can't search for both burst and not_burst - click.echo(cli.commands["export"].get_help(ctx)) - return - elif live and not_live: - # can't search for both live and not_live - click.echo(cli.commands["export"].get_help(ctx)) + exclusive = [ + (favorite, not_favorite), + (hidden, not_hidden), + (any(title), no_title), + (any(description), no_description), + (only_photos, only_movies), + (burst, not_burst), + (live, not_live), + ] + if any([all(bb) for bb in exclusive]): + click.echo(cli.commands["export"].get_help(ctx), err=True) return isphoto = ismovie = True # default searches for everything @@ -803,6 +756,8 @@ def export( not_cloudasset=False, incloud=False, not_incloud=False, + from_date=from_date, + to_date=to_date, ) if photos: @@ -978,6 +933,8 @@ def _query( not_cloudasset=None, incloud=None, not_incloud=None, + from_date=None, + to_date=None, ): """ run a query against PhotosDB to extract the photos based on user supply criteria """ """ used by query and export commands """ @@ -992,6 +949,8 @@ def _query( uuid=uuid, images=isphoto, movies=ismovie, + from_date=from_date, + to_date=to_date, ) if title: diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py index 4d9d983c..2262b7df 100644 --- a/osxphotos/photosdb.py +++ b/osxphotos/photosdb.py @@ -1318,6 +1318,8 @@ class PhotosDB: albums=None, images=True, movies=False, + from_date=None, + to_date=None, ): """ Return a list of PhotoInfo objects @@ -1328,7 +1330,7 @@ class PhotosDB: movies: if True, returns movie files, if False, does not return movies; default is False """ photos_sets = [] # list of photo sets to perform intersection of - if not keywords and not uuid and not persons and not albums: + if not any([keywords, uuid, persons, albums, from_date, to_date]): # return all the photos, filtering for images and movies # append keys of all photos as a single set to photos_sets photos_sets.append(set(self._dbphotos.keys())) @@ -1372,6 +1374,19 @@ class PhotosDB: photos_sets.append(set(self._dbfaces_person[person])) else: logging.debug(f"Could not find person '{person}' in database") + if from_date or to_date: + dsel = self._dbphotos + if from_date: + dsel = { + k: v for k, v in dsel.items() if v["imageDate"] >= from_date + } + logging.debug( + f"Found %i items with from_date {from_date}" % len(dsel) + ) + if to_date: + dsel = {k: v for k, v in dsel.items() if v["imageDate"] <= to_date} + logging.debug(f"Found %i items with to_date {to_date}" % len(dsel)) + photos_sets.append(set(dsel.keys())) photoinfo = [] if photos_sets: # found some photos diff --git a/tests/test_catalina_10_15_1.py b/tests/test_catalina_10_15_1.py index 158f1baa..360f4d85 100644 --- a/tests/test_catalina_10_15_1.py +++ b/tests/test_catalina_10_15_1.py @@ -786,3 +786,20 @@ def test_photosinfo_repr(): k: str(v).encode("utf-8") for k, v in photo2.__dict__.items() } + + +def test_from_to_date(): + import osxphotos + import datetime as dt + + photosdb = osxphotos.PhotosDB(PHOTOS_DB) + + photos = photosdb.photos(from_date=dt.datetime(2018, 10, 28)) + assert len(photos) == 2 + + photos = photosdb.photos(to_date=dt.datetime(2018, 10, 28)) + assert len(photos) == 5 + + photos = photosdb.photos(from_date=dt.datetime(2018, 9, 28), + to_date=dt.datetime(2018, 9, 29)) + assert len(photos) == 4 diff --git a/tests/test_cli.py b/tests/test_cli.py index c3e21ea7..7cf57c3c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -103,3 +103,25 @@ def test_export(): ) files = glob.glob("*.jpg") assert files.sort() == CLI_EXPORT_FILENAMES.sort() + + +def test_query_date(): + import json + import osxphotos + from osxphotos.__main__ import query + + runner = CliRunner() + result = runner.invoke( + query, + [ + "--json", + "--db", + "./tests/Test-10.15.1.photoslibrary", + "--from-date=2018-09-28", + "--to-date=2018-09-28T23:00:00" + ], + ) + assert result.exit_code == 0 + + json_got = json.loads(result.output) + assert len(json_got) == 4 \ No newline at end of file From 2908a6c3a703d8d37d2ddc9670856395d09228c8 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 07:55:13 -0800 Subject: [PATCH 10/15] Updated docs --- README.md | 10 +++++++--- osxphotos/_version.py | 2 +- osxphotos/photosdb.py | 2 ++ osxphotos/utils.py | 18 ++++++++++++++++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e19d0de7..1f15b1ed 100644 --- a/README.md +++ b/README.md @@ -449,11 +449,11 @@ photosdb.db_version Returns the version number for Photos library database. You likely won't need this but it's provided in case needed for debugging. PhotosDB will print a warning to `sys.stderr` if you open a database version that has not been tested. -#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)` +#### ` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False, from_date=None, to_date=None)` ```python # assumes photosdb is a PhotosDB object (see above) -photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]]) +photos = photosdb.photos([keywords=['keyword',]], [uuid=['uuid',]], [persons=['person',]], [albums=['album',]],[from_date=datetime.datetime],[to_date=datetime.datetime]) ``` Returns a list of [PhotoInfo](#PhotoInfo) objects. Each PhotoInfo object represents a photo in the Photos Libary. @@ -469,6 +469,8 @@ photos = photosdb.photos( albums = [], images = bool, movies = bool, + from_date = datetime.datetime, + to_date = datetime.datetime ) ``` @@ -478,8 +480,10 @@ photos = photosdb.photos( - ```albums```: list of one or more album names. Returns only photos contained in the album(s). If more than one album name is provided, returns photos contained in any of the albums (.e.g. treated as "or") - ```images```: bool; if True, returns photos/images; default is True - ```movies```: bool; if True, returns movies/videos; default is False +- ```from_date```: datetime.datetime; if provided, finds photos where creation date >= from_date; default is None +- ```to_date```: datetime.datetime; if provided, finds photos where creation date <= to_date; default is None -If more than one of (keywords, uuid, persons, albums) is provided, they are treated as "and" criteria. E.g. +If more than one of (keywords, uuid, persons, albums,from_date, to_date) is provided, they are treated as "and" criteria. E.g. Finds all photos with (keyword = "wedding" or "birthday") and (persons = "Juan Rodriguez") diff --git a/osxphotos/_version.py b/osxphotos/_version.py index 7ff27590..e9d5bf0f 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.22.3" +__version__ = "0.22.4" diff --git a/osxphotos/photosdb.py b/osxphotos/photosdb.py index 2262b7df..7020c68b 100644 --- a/osxphotos/photosdb.py +++ b/osxphotos/photosdb.py @@ -1328,6 +1328,8 @@ class PhotosDB: If more than one arg, returns photos matching all the criteria (e.g. keywords AND persons) images: if True, returns image files, if False, does not return images; default is True movies: if True, returns movie files, if False, does not return movies; default is False + from_date: return photos with creation date >= from_date (datetime.datetime object, default None) + to_date: return photos with creation date <= to_date (datetime.datetime object, default None) """ photos_sets = [] # list of photo sets to perform intersection of if not any([keywords, uuid, persons, albums, from_date, to_date]): diff --git a/osxphotos/utils.py b/osxphotos/utils.py index ef39f44b..e73ac5f6 100644 --- a/osxphotos/utils.py +++ b/osxphotos/utils.py @@ -289,6 +289,24 @@ def create_path_by_date(dest, dt): return new_dest +# TODO: this doesn't always work, still looking for a way to +# force Photos to open the library being operated on +# def _open_photos_library_applescript(library_path): +# """ Force Photos to open a specific library +# library_path: path to the Photos library """ +# open_scpt = AppleScript( +# f""" +# on openLibrary +# tell application "Photos" +# activate +# open POSIX file "{library_path}" +# end tell +# end openLibrary +# """ +# ) +# open_scpt.run() + + def _export_photo_uuid_applescript( uuid, dest, original=True, edited=False, timeout=120 ): From d1afd55a7c7c4d413a1afc423975171c6c5bfb09 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 07:58:03 -0800 Subject: [PATCH 11/15] Updated README.md --- README.md | 51 +-------------------------------------------------- 1 file changed, 1 insertion(+), 50 deletions(-) diff --git a/README.md b/README.md index 1f15b1ed..6e739f58 100644 --- a/README.md +++ b/README.md @@ -11,63 +11,14 @@ * [Example uses of the module](#example-uses-of-the-module) * [Module Interface](#module-interface) + [PhotosDB](#photosdb) - - [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) - - [`albums`](#albums) - - [`albums_shared`](#albums_shared) - - [`persons`](#persons) - - [`keywords_as_dict`](#keywords_as_dict) - - [`persons_as_dict`](#persons_as_dict) - - [`albums_as_dict`](#albums_as_dict) - - [`albums_shared_as_dict`](#albums_shared_as_dict) - - [`library_path`](#library_path) - - [`db_path`](#db_path) - - [`db_version`](#db_version) - - [` photos(keywords=None, uuid=None, persons=None, albums=None, images=True, movies=False)`](#-photoskeywordsnone-uuidnone-personsnone-albumsnone-imagestrue-moviesfalse) + [PhotoInfo](#photoinfo) - - [`uuid`](#uuid) - - [`filename`](#filename) - - [`original_filename`](#original_filename) - - [`date`](#date) - - [`description`](#description) - - [`title`](#title) - - [`keywords`](#keywords-1) - - [`albums`](#albums-1) - - [`persons`](#persons-1) - - [`path`](#path) - - [`path_edited`](#path_edited) - - [`ismissing`](#ismissing) - - [`hasadjustments`](#hasadjustments) - - [`external_edit`](#external_edit) - - [`favorite`](#favorite) - - [`hidden`](#hidden) - - [`location`](#location) - - [`shared`](#shared) - - [`isphoto`](#isphoto) - - [`ismovie`](#ismovie) - - [`iscloudasset`](#iscloudasset) - - [`incloud`](#incloud) - - [`uti`](#uti) - - [`burst`](#burst) - - [`burst_photos`](#burst_photos) - - [`live_photo`](#live_photo) - - [`path_live_photo`](#path_live_photo) - - [`json()`](#json) - - [`export(dest, *filename, edited=False, overwrite=False, increment=True, sidecar=False, use_photos_export=False, timeout=120)`](#exportdest-filename-editedfalse-overwritefalse-incrementtrue-sidecarfalse-use_photos_exportfalse-timeout120) + [Utility Functions](#utility-functions) - - [```get_system_library_path()```](#get_system_library_path) - - [```get_last_library_path()```](#get_last_library_path) - - [```list_photo_libraries()```](#list_photo_libraries) - - [```dd_to_dms_str(lat, lon)```](#dd_to_dms_strlat-lon) - - [```create_path_by_date(dest, dt)```](#create_path_by_datedest-dt) + [Examples](#examples) * [Related Projects](#related-projects) * [Contributing](#contributing) * [Implementation Notes](#implementation-notes) * [Dependencies](#dependencies) - * [Acknowledgements](#acknowledgements) + * [Acknowledgements](#acknowledgements) ## What is osxphotos? From 0aff83ff21c20e293c0b75bacf2863090a0fb725 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 08:02:03 -0800 Subject: [PATCH 12/15] Updated README.md --- README.md | 160 +++++++++++++++++++++++++++++------------------------- 1 file changed, 87 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 6e739f58..5488f52b 100644 --- a/README.md +++ b/README.md @@ -84,79 +84,93 @@ Usage: osxphotos export [OPTIONS] [PHOTOS_LIBRARY]... DEST photos will be exported. Options: - --db 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. + --db Specify Photos database path. Path to Photos + library/database can be specified using + either --db or directly as PHOTOS_LIBRARY + positional argument. + --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. + --shared Search for photos in shared iCloud album + (Photos 5 only). + --not-shared Search for photos not in shared iCloud album + (Photos 5 only). + --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 + --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). + --from-date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S] + Search by start item date, e.g. + 2000-01-12T12:00:00 or 2000-12-31 (ISO 8601 + w/o TZ). + --to-date [%Y-%m-%d|%Y-%m-%dT%H:%M:%S|%Y-%m-%d %H:%M:%S] + Search by end item date, e.g. + 2000-01-12T12:00:00 or 2000-12-31 (ISO 8601 + w/o TZ). + -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. + --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 From d37e6d9725ab2b91d942ba6f234d24c2807e8749 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 08:05:32 -0800 Subject: [PATCH 13/15] Updated CHANGELOG.md --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4cf2a4c6..ed15efd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [v0.22.4](https://github.com/RhetTbull/osxphotos/compare/v0.22.0...v0.22.4) + +> 20 January 2020 + +- Add --from-date and --to-date to query and export command [`#57`](https://github.com/RhetTbull/osxphotos/pull/57) +- Refactor CLI [`#55`](https://github.com/RhetTbull/osxphotos/pull/55) +- Refactor cli: singular --db, --json and query options. [`e214746`](https://github.com/RhetTbull/osxphotos/commit/e214746063271e6f9f586286103ed051ada49d85) +- Implement from_date and to_date in PhotosDB as well as query and export command. Some refactoring of CLI as well. [`cfa2b4a`](https://github.com/RhetTbull/osxphotos/commit/cfa2b4a828facf0aff5bc19f777457ad776c4a05) +- Refactored _query. Still hairy, but less so. [`b9dee49`](https://github.com/RhetTbull/osxphotos/commit/b9dee4995c6d89fadb3d2482374b7098f2ab5ed9) +- Updated README.md [`0aff83f`](https://github.com/RhetTbull/osxphotos/commit/0aff83ff21c20e293c0b75bacf2863090a0fb725) +- Started adding tests for CLI [`f0b18c3`](https://github.com/RhetTbull/osxphotos/commit/f0b18c3d29b2141d348be0495013c51c072c6251) + #### [v0.22.0](https://github.com/RhetTbull/osxphotos/compare/v0.21.5...v0.22.0) > 18 January 2020 From 50b7e6920a694aa45f478d1131868525c9147919 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 08:26:32 -0800 Subject: [PATCH 14/15] CLI now looks for photos library to use if non specified by user --- README.md | 7 +++++- osxphotos/__main__.py | 50 +++++++++++++++++++++---------------------- osxphotos/_version.py | 2 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 5488f52b..c47f430b 100644 --- a/README.md +++ b/README.md @@ -87,7 +87,12 @@ Options: --db Specify Photos database path. Path to Photos library/database can be specified using either --db or directly as PHOTOS_LIBRARY - positional argument. + positional argument. If neither --db or + PHOTOS_LIBRARY provided, will attempt to + find the library to use in the following + order: 1. last opened library, 2. system + library, 3. ~/Pictures/Photos + Library.photoslibrary --keyword TEXT Search for keyword(s). --person TEXT Search for person(s). --album TEXT Search for album(s). diff --git a/osxphotos/__main__.py b/osxphotos/__main__.py index 74333e9d..1d1d2ad4 100644 --- a/osxphotos/__main__.py +++ b/osxphotos/__main__.py @@ -17,38 +17,36 @@ from .utils import create_path_by_date, _copy_file def get_photos_db(*db_options): - """ Return path to photos db, select first non-None arg + """ Return path to photos db, select first non-None db_options + If no db_options are non-None, try to find library to use in + the following order: + - last library opened + - system library + - ~/Pictures/Photos Library.photoslibrary + - failing above, returns None """ 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 try to figure out which to use + db = osxphotos.utils.get_last_library_path() + if db is not None: + click.echo(f"Using last opened Photos library: {db}", err=True) + return db - # if get here, no valid database paths passed, so ask user + db = osxphotos.utils.get_system_library_path() + if db is not None: + click.echo(f"Using system Photos library: {db}", err=True) + return db - # _, 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 + db = os.path.expanduser("~/Pictures/Photos Library.photoslibrary") + if os.path.isdir(db): + click.echo(f"Using Photos library: {db}", err=True) + return db + else: + return None # Click CLI object & context settings @@ -69,7 +67,9 @@ DB_OPTION = click.option( help=( "Specify Photos database path. " "Path to Photos library/database can be specified using either --db " - "or directly as PHOTOS_LIBRARY positional argument." + "or directly as PHOTOS_LIBRARY positional argument. " + "If neither --db or PHOTOS_LIBRARY provided, will attempt to find the library " + "to use in the following order: 1. last opened library, 2. system library, 3. ~/Pictures/Photos Library.photoslibrary" ), type=click.Path(exists=True), ) diff --git a/osxphotos/_version.py b/osxphotos/_version.py index e9d5bf0f..ed3ae623 100644 --- a/osxphotos/_version.py +++ b/osxphotos/_version.py @@ -1,3 +1,3 @@ """ version info """ -__version__ = "0.22.4" +__version__ = "0.22.5" From db5effde52af29d59253258d0a1dc54548856713 Mon Sep 17 00:00:00 2001 From: Rhet Turnbull Date: Mon, 20 Jan 2020 08:59:52 -0800 Subject: [PATCH 15/15] Added photos_repl.py to examples --- examples/photos_repl.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100755 examples/photos_repl.py diff --git a/examples/photos_repl.py b/examples/photos_repl.py new file mode 100755 index 00000000..322de755 --- /dev/null +++ b/examples/photos_repl.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 -i + +# open an interactive REPL with photosdb and photos defined +# as osxphotos.PhotosDB() and PhotosDB.photos respectively +# useful for debugging or exploring the Photos database + +import sys + +# click needed since this uses a couple of functions from CLI (__main__.py) +import click + +import osxphotos +from osxphotos.__main__ import get_photos_db, _list_libraries + + +def main(): + db = None + + if len(sys.argv) > 1: + db = sys.argv[1] + else: + db = get_photos_db() + + if db: + return osxphotos.PhotosDB(dbfile=db) + else: + _list_libraries() + sys.exit() + + +if __name__ == "__main__": + print(f"Version: {osxphotos._version.__version__}") + photosdb = main() + photos = photosdb.photos(images=True, movies=True)