Skip to content

Commit 5ce5a8e

Browse files
add skip-created-before param #1111 #466 (#1145)
1 parent 99d0c68 commit 5ce5a8e

File tree

7 files changed

+287
-69
lines changed

7 files changed

+287
-69
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- feat: `--skip-created-before` to limit assets by creation date [#466](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/466) [#1111](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1111)
6+
57
## 1.27.5 (2025-05-08)
68

79
- fix: HEIF file extension [#1133](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1133)

docs/reference.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,3 +301,14 @@ This is a list of all options available for command line interface (CLI) of the
301301

302302
: Script to be executed for notification on expired MFA
303303

304+
(skip-created-before-parameter)=
305+
`--skip-created-before`
306+
307+
: Does not process assets created before specified timestamp. Timestamp is in ISO format, e.g 2025-06-01, or as interval from now, e.g. 5d. If timezone is not specified for ISO format, then local timezone is used.
308+
309+
```{versionadded} 1.28.0
310+
```
311+
312+
```{note}
313+
The date is when asset was created, not added to the iCloud.
314+
```

src/icloudpd/base.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22
"""Main script that uses Click to parse command-line arguments"""
33

4+
import re
45
from multiprocessing import freeze_support
56

67
import foundation
@@ -32,6 +33,7 @@
3233
Sequence,
3334
Tuple,
3435
TypeVar,
36+
Union,
3537
cast,
3638
)
3739

@@ -260,6 +262,28 @@ def file_match_policy_generator(
260262
raise ValueError(f"policy was provided with unsupported value of '{policy}'")
261263

262264

265+
def skip_created_before_generator(
266+
_ctx: click.Context, _param: click.Parameter, formatted: str
267+
) -> Optional[Union[datetime.datetime, datetime.timedelta]]:
268+
if formatted is None:
269+
return None
270+
# can be timestamp or timedelta
271+
m = re.match(r"(\d+)([dD]{1})", formatted)
272+
if m is not None and m.lastindex is not None and m.lastindex == 2:
273+
return datetime.timedelta(days=float(m.group(1)))
274+
275+
# try timestamp
276+
try:
277+
dt = datetime.datetime.fromisoformat(formatted)
278+
if dt.tzinfo is None:
279+
dt = dt.astimezone(get_localzone())
280+
return dt
281+
except Exception as e:
282+
raise ValueError(
283+
f"Timestamp {formatted} for --skip-created-before parameter did not parse from ISO format successfully: {e}"
284+
) from e
285+
286+
263287
def locale_setter(_ctx: click.Context, _param: click.Parameter, use_os_locale: bool) -> bool:
264288
# set locale
265289
if use_os_locale:
@@ -570,6 +594,11 @@ def report_version(ctx: click.Context, _param: click.Parameter, value: bool) ->
570594
is_eager=True,
571595
callback=locale_setter,
572596
)
597+
@click.option(
598+
"--skip-created-before",
599+
help="Do not process assets created before specified timestamp in ISO format (2025-01-02) or interval from now (20d)",
600+
callback=skip_created_before_generator,
601+
)
573602
@click.option(
574603
"--version",
575604
help="Show the version, commit hash and timestamp",
@@ -625,6 +654,7 @@ def main(
625654
file_match_policy: FileMatchPolicy,
626655
mfa_provider: MFAProvider,
627656
use_os_locale: bool,
657+
skip_created_before: Optional[Union[datetime.datetime, datetime.timedelta]],
628658
) -> NoReturn:
629659
"""Download all iCloud photos to a local directory"""
630660

@@ -778,6 +808,7 @@ def main(
778808
dry_run,
779809
file_match_policy,
780810
xmp_sidecar,
811+
skip_created_before,
781812
)
782813
if directory is not None
783814
else (lambda _s: lambda _c, _p: False),
@@ -836,6 +867,7 @@ def download_builder(
836867
dry_run: bool,
837868
file_match_policy: FileMatchPolicy,
838869
xmp_sidecar: bool,
870+
skip_created_before: Optional[Union[datetime.datetime, datetime.timedelta]],
839871
) -> Callable[[PyiCloudService], Callable[[Counter, PhotoAsset], bool]]:
840872
"""factory for downloader"""
841873

@@ -866,6 +898,23 @@ def download_photo_(counter: Counter, photo: PhotoAsset) -> bool:
866898
)
867899
created_date = photo.created
868900

901+
if skip_created_before is not None:
902+
if isinstance(skip_created_before, datetime.timedelta):
903+
temp_created_before = (
904+
datetime.datetime.now(get_localzone()) - skip_created_before
905+
)
906+
elif isinstance(skip_created_before, datetime.datetime):
907+
temp_created_before = skip_created_before
908+
else:
909+
raise ValueError(
910+
f"skip-created-before is of unsupported type {type(skip_created_before)}"
911+
)
912+
if created_date < temp_created_before:
913+
logger.debug(
914+
f"Skipping {photo.filename}, as it was created {created_date}, before {temp_created_before}."
915+
)
916+
return False
917+
869918
if folder_structure.lower() == "none":
870919
date_path = ""
871920
else:

src/icloudpd/email_notifications.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,7 @@ def send_2sa_notification(
4444
{username}'s two-step authentication has expired for the icloud_photos_downloader script.
4545
Please log in to your server and run the script manually to update two-step authentication."""
4646

47-
msg = (
48-
f"From: {from_addr}\n"
49-
+ f"To: {to_addr}\nSubject: {subj}\nDate: {date}\n\n{message_text}"
50-
)
47+
msg = f"From: {from_addr}\n" + f"To: {to_addr}\nSubject: {subj}\nDate: {date}\n\n{message_text}"
5148

5249
smtp.sendmail(from_addr, to_addr, msg)
5350
smtp.quit()

tests/test_download_photos.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2389,3 +2389,88 @@ def test_download_from_shared_library(self) -> None:
23892389
self._caplog.text,
23902390
)
23912391
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
2392+
2393+
def test_download_and_skip_old(self) -> None:
2394+
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
2395+
2396+
files_to_create: List[Tuple[str, str, int]] = [
2397+
# ("2018/07/30", "IMG_7408.JPG", 1151066),
2398+
# ("2018/07/30", "IMG_7407.JPG", 656257),
2399+
]
2400+
2401+
files_to_download = [("2018/07/31", "IMG_7409.JPG")]
2402+
2403+
data_dir, result = run_icloudpd_test(
2404+
self.assertEqual,
2405+
self.root_path,
2406+
base_dir,
2407+
"listing_photos.yml",
2408+
files_to_create,
2409+
files_to_download,
2410+
[
2411+
"--username",
2412+
"jdoe@gmail.com",
2413+
"--password",
2414+
"password1",
2415+
"--recent",
2416+
"5",
2417+
"--skip-videos",
2418+
"--skip-live-photos",
2419+
"--set-exif-datetime",
2420+
"--no-progress-bar",
2421+
"--skip-created-before",
2422+
"2018-07-31",
2423+
],
2424+
)
2425+
2426+
assert result.exit_code == 0
2427+
2428+
self.assertIn("DEBUG Looking up all photos...", self._caplog.text)
2429+
self.assertIn(
2430+
f"INFO Downloading 5 original photos to {data_dir} ...",
2431+
self._caplog.text,
2432+
)
2433+
for dir_name, file_name in files_to_download:
2434+
file_path = os.path.normpath(os.path.join(dir_name, file_name))
2435+
self.assertIn(
2436+
f"DEBUG Downloading {os.path.join(data_dir, file_path)}",
2437+
self._caplog.text,
2438+
)
2439+
self.assertNotIn(
2440+
"IMG_7409.MOV",
2441+
self._caplog.text,
2442+
)
2443+
for dir_name, file_name in [
2444+
(dir_name, file_name) for (dir_name, file_name, _) in files_to_create
2445+
]:
2446+
file_path = os.path.normpath(os.path.join(dir_name, file_name))
2447+
self.assertIn(
2448+
f"DEBUG {os.path.join(data_dir, file_path)} already exists",
2449+
self._caplog.text,
2450+
)
2451+
2452+
self.assertIn(
2453+
"DEBUG Skipping IMG_7405.MOV, only downloading photos.",
2454+
self._caplog.text,
2455+
)
2456+
self.assertIn(
2457+
"DEBUG Skipping IMG_7404.MOV, only downloading photos.",
2458+
self._caplog.text,
2459+
)
2460+
self.assertIn(
2461+
"DEBUG Skipping IMG_7407.JPG, as it was created 2018-07-30 11:44:05.108000+00:00, before 2018-07-31 00:00:00+00:00.",
2462+
self._caplog.text,
2463+
)
2464+
self.assertIn(
2465+
"DEBUG Skipping IMG_7408.JPG, as it was created 2018-07-30 11:44:10.176000+00:00, before 2018-07-31 00:00:00+00:00.",
2466+
self._caplog.text,
2467+
)
2468+
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
2469+
2470+
# Check that file was downloaded
2471+
# Check that mtime was updated to the photo creation date
2472+
photo_mtime = os.path.getmtime(
2473+
os.path.join(data_dir, os.path.normpath("2018/07/31/IMG_7409.JPG"))
2474+
)
2475+
photo_modified_time = datetime.datetime.fromtimestamp(photo_mtime, datetime.timezone.utc)
2476+
self.assertEqual("2018-07-31 07:22:24", photo_modified_time.strftime("%Y-%m-%d %H:%M:%S"))

0 commit comments

Comments
 (0)