diff --git a/README.md b/README.md
index 77208451..a0b98905 100644
--- a/README.md
+++ b/README.md
@@ -738,6 +738,10 @@ Options:
or 12:00:00.
--to-time TIME Search by item end time of day, e.g. 12:00 or
12:00:00.
+ --year INTEGER Search for items from a specific year, e.g.
+ --year 2022 to find all photos from the year
+ 2022. May be repeated to search multiple
+ years.
--has-comment Search for photos that have comments.
--no-comment Search for photos with no comments.
--has-likes Search for photos that have likes.
diff --git a/docs/_modules/osxphotos/photosdb/photosdb.html b/docs/_modules/osxphotos/photosdb/photosdb.html
index 1809d7d9..fc73d05d 100644
--- a/docs/_modules/osxphotos/photosdb/photosdb.html
+++ b/docs/_modules/osxphotos/photosdb/photosdb.html
@@ -5,7 +5,7 @@
- osxphotos.photosdb.photosdb — osxphotos 0.47.5 documentation
+ osxphotos.photosdb.photosdb — osxphotos 0.47.8 documentation
@@ -3322,6 +3322,9 @@
ifoptions.to_time:photos=[pforpinphotosifp.date.time()<=options.to_time]
+ ifoptions.year:
+ photos=[pforpinphotosifp.date.yearinoptions.year]
+
ifname:# search filename fields for text# if more than one, find photos with all title values in filename
diff --git a/osxphotos/cli/common.py b/osxphotos/cli/common.py
index 9c6114cc..12844bcf 100644
--- a/osxphotos/cli/common.py
+++ b/osxphotos/cli/common.py
@@ -368,6 +368,13 @@ def QUERY_OPTIONS(f):
help="Search by item end time of day, e.g. 12:00 or 12:00:00.",
type=TimeISO8601(),
),
+ o(
+ "--year",
+ help="Search for items from a specific year, e.g. --year 2022 to find all photos from the year 2022. "
+ "May be repeated to search multiple years.",
+ multiple=True,
+ type=int,
+ ),
o("--has-comment", is_flag=True, help="Search for photos that have comments."),
o("--no-comment", is_flag=True, help="Search for photos with no comments."),
o("--has-likes", is_flag=True, help="Search for photos that have likes."),
diff --git a/osxphotos/cli/export.py b/osxphotos/cli/export.py
index b01e4fe2..ac059ff9 100644
--- a/osxphotos/cli/export.py
+++ b/osxphotos/cli/export.py
@@ -697,6 +697,7 @@ def export(
to_date,
from_time,
to_time,
+ year,
verbose,
timestamp,
no_progress,
@@ -1018,6 +1019,7 @@ def export(
uuid_from_file = cfg.uuid_from_file
verbose = cfg.verbose
xattr_template = cfg.xattr_template
+ year = cfg.year
# config file might have changed verbose
color_theme = get_theme(theme)
@@ -1300,6 +1302,7 @@ def export(
to_date=to_date,
from_time=from_time,
to_time=to_time,
+ year=year,
portrait=portrait,
not_portrait=not_portrait,
screenshot=screenshot,
diff --git a/osxphotos/cli/query.py b/osxphotos/cli/query.py
index efbe7bc6..b9642b2d 100644
--- a/osxphotos/cli/query.py
+++ b/osxphotos/cli/query.py
@@ -111,6 +111,7 @@ def query(
to_date,
from_time,
to_time,
+ year,
portrait,
not_portrait,
screenshot,
@@ -174,6 +175,7 @@ def query(
to_date,
from_time,
to_time,
+ year,
label,
is_reference,
query_eval,
@@ -280,6 +282,7 @@ def query(
to_date=to_date,
from_time=from_time,
to_time=to_time,
+ year=year,
portrait=portrait,
not_portrait=not_portrait,
screenshot=screenshot,
diff --git a/osxphotos/cli/repl.py b/osxphotos/cli/repl.py
index 44d87bb0..d4c6c3d8 100644
--- a/osxphotos/cli/repl.py
+++ b/osxphotos/cli/repl.py
@@ -256,6 +256,7 @@ def _query_options_from_kwargs(**kwargs) -> QueryOptions:
"to_date",
"from_time",
"to_time",
+ "year",
"label",
"is_reference",
"query_eval",
diff --git a/osxphotos/docs/_modules/osxphotos/photosdb/photosdb.html b/osxphotos/docs/_modules/osxphotos/photosdb/photosdb.html
index 1809d7d9..fc73d05d 100644
--- a/osxphotos/docs/_modules/osxphotos/photosdb/photosdb.html
+++ b/osxphotos/docs/_modules/osxphotos/photosdb/photosdb.html
@@ -5,7 +5,7 @@
- osxphotos.photosdb.photosdb — osxphotos 0.47.5 documentation
+ osxphotos.photosdb.photosdb — osxphotos 0.47.8 documentation
@@ -3322,6 +3322,9 @@
ifoptions.to_time:photos=[pforpinphotosifp.date.time()<=options.to_time]
+ ifoptions.year:
+ photos=[pforpinphotosifp.date.yearinoptions.year]
+
ifname:# search filename fields for text# if more than one, find photos with all title values in filename
diff --git a/osxphotos/photosdb/photosdb.py b/osxphotos/photosdb/photosdb.py
index c8a1f9eb..311c6587 100644
--- a/osxphotos/photosdb/photosdb.py
+++ b/osxphotos/photosdb/photosdb.py
@@ -3289,6 +3289,9 @@ class PhotosDB:
if options.to_time:
photos = [p for p in photos if p.date.time() <= options.to_time]
+ if options.year:
+ photos = [p for p in photos if p.date.year in options.year]
+
if name:
# search filename fields for text
# if more than one, find photos with all title values in filename
diff --git a/osxphotos/queryoptions.py b/osxphotos/queryoptions.py
index 813605f9..f81c9f65 100644
--- a/osxphotos/queryoptions.py
+++ b/osxphotos/queryoptions.py
@@ -87,6 +87,7 @@ class QueryOptions:
function: Optional[List[Tuple[callable, str]]] = None
selected: Optional[bool] = None
exif: Optional[Iterable[Tuple[str, str]]] = None
+ year: Optional[int] = None
def asdict(self):
return asdict(self)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 5b18d2e5..6db77358 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -740,6 +740,8 @@ CLI_FINDER_TAGS = {
},
}
+CLI_EXPORT_YEAR_2017 = ["IMG_4547.jpg"]
+
LABELS_JSON = {
"labels": {
"Water": 2,
@@ -1494,6 +1496,28 @@ def test_export_skip_uuid():
assert skipped_file not in files
+def test_export_year():
+ """test export with --year"""
+ runner = CliRunner()
+ cwd = os.getcwd()
+ # pylint: disable=not-context-manager
+ with runner.isolated_filesystem():
+ result = runner.invoke(
+ export,
+ [
+ os.path.join(cwd, CLI_PHOTOS_DB),
+ ".",
+ "-V",
+ "--year",
+ "2017",
+ "--skip-edited",
+ ],
+ )
+ assert result.exit_code == 0
+ files = glob.glob("*")
+ assert sorted(files) == sorted(CLI_EXPORT_YEAR_2017)
+
+
def test_export_preview():
"""test export with --preview"""
@@ -2572,6 +2596,80 @@ def test_query_time():
assert len(json_got) == 3
+def test_query_year_1():
+ """Test --year"""
+
+ os.environ["TZ"] = "US/Pacific"
+ time.tzset()
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ result = runner.invoke(
+ query,
+ [
+ "--json",
+ "--db",
+ os.path.join(cwd, CLI_PHOTOS_DB),
+ "--year",
+ 2017,
+ ],
+ )
+ assert result.exit_code == 0
+
+ json_got = json.loads(result.output)
+ assert len(json_got) == 1
+
+
+def test_query_year_2():
+ """Test --year with multiple years"""
+
+ os.environ["TZ"] = "US/Pacific"
+ time.tzset()
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ result = runner.invoke(
+ query,
+ [
+ "--json",
+ "--db",
+ os.path.join(cwd, CLI_PHOTOS_DB),
+ "--year",
+ 2017,
+ "--year",
+ 2018,
+ ],
+ )
+ assert result.exit_code == 0
+
+ json_got = json.loads(result.output)
+ assert len(json_got) == 6
+
+
+def test_query_year_3():
+ """Test --year with invalid year"""
+
+ os.environ["TZ"] = "US/Pacific"
+ time.tzset()
+
+ runner = CliRunner()
+ cwd = os.getcwd()
+ result = runner.invoke(
+ query,
+ [
+ "--json",
+ "--db",
+ os.path.join(cwd, CLI_PHOTOS_DB),
+ "--year",
+ 3000,
+ ],
+ )
+ assert result.exit_code == 0
+
+ json_got = json.loads(result.output)
+ assert len(json_got) == 0
+
+
def test_query_keyword_1():
"""Test query --keyword"""