diff --git a/osxphotos/cli/common.py b/osxphotos/cli/common.py index 892fa98a..f7242c05 100644 --- a/osxphotos/cli/common.py +++ b/osxphotos/cli/common.py @@ -146,6 +146,11 @@ def QUERY_OPTIONS(f): help="Search for photos with keyword KEYWORD. " 'If more than one keyword, treated as "OR", e.g. find photos matching any keyword', ), + o( + "--no-keyword", + is_flag=True, + help="Search for photos with no keyword.", + ), o( "--person", metavar="PERSON", diff --git a/osxphotos/cli/export.py b/osxphotos/cli/export.py index d6e6d255..3dcfe65b 100644 --- a/osxphotos/cli/export.py +++ b/osxphotos/cli/export.py @@ -749,6 +749,7 @@ def export( no_description, no_likes, no_location, + no_keyword, no_place, no_progress, no_title, @@ -964,6 +965,7 @@ def export( no_description = cfg.no_description no_likes = cfg.no_likes no_location = cfg.no_location + no_keyword = cfg.no_keyword no_place = cfg.no_place no_progress = cfg.no_progress no_title = cfg.no_title @@ -1068,6 +1070,7 @@ def export( ("in_album", "not_in_album"), ("live", "not_live"), ("location", "no_location"), + ("keyword", "no_keyword"), ("only_photos", "only_movies"), ("panorama", "not_panorama"), ("place", "no_place"), @@ -1325,6 +1328,7 @@ def export( no_description=no_description, no_likes=no_likes, no_location=no_location, + no_keyword=no_keyword, no_place=no_place, no_title=no_title, not_burst=not_burst, diff --git a/osxphotos/cli/query.py b/osxphotos/cli/query.py index 65f5f096..9ce4df37 100644 --- a/osxphotos/cli/query.py +++ b/osxphotos/cli/query.py @@ -113,6 +113,7 @@ def query( no_description, no_likes, no_location, + no_keyword, no_place, no_title, not_burst, @@ -197,6 +198,7 @@ def query( (any(description), no_description), (any(place), no_place), (any(title), no_title), + (any(keyword), no_keyword), (burst, not_burst), (cloudasset, not_cloudasset), (deleted, deleted_only), @@ -293,6 +295,7 @@ def query( no_description=no_description, no_likes=no_likes, no_location=no_location, + no_keyword=no_keyword, no_place=no_place, no_title=no_title, not_burst=not_burst, diff --git a/osxphotos/cli/repl.py b/osxphotos/cli/repl.py index 25f71dfd..5b5838bd 100644 --- a/osxphotos/cli/repl.py +++ b/osxphotos/cli/repl.py @@ -284,6 +284,7 @@ def _query_options_from_kwargs(**kwargs) -> QueryOptions: ("incloud", "not_incloud"), ("live", "not_live"), ("location", "no_location"), + ("keyword", "no_keyword"), ("missing", "not_missing"), ("only_photos", "only_movies"), ("panorama", "not_panorama"), @@ -301,6 +302,7 @@ def _query_options_from_kwargs(**kwargs) -> QueryOptions: all([any(kwargs["title"]), kwargs["no_title"]]), all([any(kwargs["description"]), kwargs["no_description"]]), all([any(kwargs["place"]), kwargs["no_place"]]), + all([any(kwargs["keyword"]), kwargs["no_keyword"]]), ] ): raise IncompatibleQueryOptions diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py index 38429663..2b9dddbb 100644 --- a/osxphotos/photosdb/photosdb.py +++ b/osxphotos/photosdb/photosdb.py @@ -3076,6 +3076,8 @@ class PhotosDB: photos = _get_photos_by_attribute( photos, "keywords", keyword, options.ignore_case ) + elif options.no_keyword: + photos = [p for p in photos if not p.keywords] if person: photos = _get_photos_by_attribute( diff --git a/osxphotos/queryoptions.py b/osxphotos/queryoptions.py index 439d91a4..08cf1ea5 100644 --- a/osxphotos/queryoptions.py +++ b/osxphotos/queryoptions.py @@ -56,6 +56,7 @@ class QueryOptions: no_description: search for photos with no description no_likes: search for shared photos with no likes no_location: search for photos with no location + no_keyword: search for photos with no keywords no_place: search for photos with no place no_title: search for photos with no title not_burst: search for non-burst photos @@ -136,6 +137,7 @@ class QueryOptions: no_description: Optional[bool] = None no_likes: Optional[bool] = None no_location: Optional[bool] = None + no_keyword: Optional[bool] = None no_place: Optional[bool] = None no_title: Optional[bool] = None not_burst: Optional[bool] = None diff --git a/tests/test_cli.py b/tests/test_cli.py index d7e819dc..10ca86ea 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -2711,6 +2711,27 @@ def test_query_keyword_4(): assert len(json_got) == 6 +def test_query_no_keyword(): + """Test query --no-keyword""" + + runner = CliRunner() + cwd = os.getcwd() + result = runner.invoke( + query, + [ + "--json", + "--db", + os.path.join(cwd, PHOTOS_DB_15_7), + "--no-keyword", + "--added-before", + "2022-05-05", + ], + ) + assert result.exit_code == 0 + json_got = json.loads(result.output) + assert len(json_got) == 11 + + def test_query_person_1(): """Test query --person""" @@ -7772,3 +7793,23 @@ def test_export_limit(): ) assert result.exit_code == 0 assert "limit: 0/20 exported" in result.output + +def test_export_no_keyword(): + """test export --no-keyword""" + + runner = CliRunner() + cwd = os.getcwd() + with runner.isolated_filesystem(): + result = runner.invoke( + export, + [ + ".", + "--db", + os.path.join(cwd, PHOTOS_DB_15_7), + "--no-keyword", + "--added-before", + "2022-05-05", + ], + ) + assert result.exit_code == 0 + assert "Exporting 11" in result.output \ No newline at end of file