Skip to content

Commit 0bfe88f

Browse files
add skip-photos option #401 (#1213)
1 parent 920b009 commit 0bfe88f

File tree

10 files changed

+11898
-45
lines changed

10 files changed

+11898
-45
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: add `--skip-photos` filter [#401](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/401) [#1206](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1206)
6+
57
## 1.29.4 (2025-08-12)
68

79
- fix: failed auth terminate webui [#1195](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1195)

docs/reference.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ This is a list of all options available for command line interface (CLI) of the
165165

166166
: If specified, video assets will not be processed
167167

168+
(skip-photos-parameter)=
169+
`--skip-photos`
170+
171+
```{versionadded} 1.30.0
172+
```
173+
174+
175+
: If specified, photos assets will not be processed
176+
168177
(skip-live-photos-parameter)=
169178
`--skip-live-photos`
170179

src/icloudpd/base.py

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,11 @@ def report_version(ctx: click.Context, _param: click.Parameter, value: bool) ->
625625
help="Do not process assets created after specified timestamp in ISO format (2025-01-02) or interval from now (20d)",
626626
callback=skip_created_after_generator,
627627
)
628+
@click.option(
629+
"--skip-photos",
630+
help="Don't download any photos (default: Download all photos and videos)",
631+
is_flag=True,
632+
)
628633
@click.option(
629634
"--version",
630635
help="Show the version, commit hash and timestamp",
@@ -680,6 +685,7 @@ def main(
680685
use_os_locale: bool,
681686
skip_created_before: datetime.datetime | datetime.timedelta | None,
682687
skip_created_after: datetime.datetime | datetime.timedelta | None,
688+
skip_photos: bool,
683689
) -> NoReturn:
684690
"""Download all iCloud photos to a local directory"""
685691

@@ -703,6 +709,10 @@ def main(
703709
logger.setLevel(logging.ERROR)
704710

705711
with logging_redirect_tqdm():
712+
if skip_videos and skip_photos:
713+
print("Only one of --skip-videos and --skip-photos can be used at a time")
714+
sys.exit(2)
715+
706716
# check required directory param only if not list albums
707717
if not list_albums and not list_libraries and not directory and not auth_only:
708718
print("--auth-only, --directory, --list-libraries or --list-albums are required")
@@ -826,6 +836,7 @@ def main(
826836
skip_videos,
827837
skip_created_before,
828838
skip_created_after,
839+
skip_photos,
829840
)
830841
downloader = (
831842
partial(
@@ -892,6 +903,7 @@ def main(
892903
password_providers,
893904
mfa_provider,
894905
status_exchange,
906+
skip_photos,
895907
)
896908
sys.exit(result)
897909

@@ -938,6 +950,7 @@ def where_builder(
938950
skip_videos: bool,
939951
skip_created_before: datetime.datetime | datetime.timedelta | None,
940952
skip_created_after: datetime.datetime | datetime.timedelta | None,
953+
skip_photos: bool,
941954
photo: PhotoAsset,
942955
) -> bool:
943956
if skip_videos and photo.item_type == AssetItemType.MOVIE:
@@ -947,6 +960,13 @@ def where_builder(
947960
photo.item_type,
948961
)
949962
return False
963+
if skip_photos and photo.item_type == AssetItemType.IMAGE:
964+
logger.debug(
965+
"Skipping %s, only downloading videos." + "(Item type was: %s)",
966+
photo.filename,
967+
photo.item_type,
968+
)
969+
return False
950970

951971
try:
952972
created_date = photo.created.astimezone(get_localzone())
@@ -1320,6 +1340,7 @@ def core(
13201340
password_providers: Dict[str, Tuple[Callable[[str], str | None], Callable[[str, str], None]]],
13211341
mfa_provider: MFAProvider,
13221342
status_exchange: StatusExchange,
1343+
skip_photos: bool,
13231344
) -> int:
13241345
"""Download all iCloud photos to a local directory"""
13251346

@@ -1394,9 +1415,12 @@ def append_response(captured: List[Mapping[str, Any]], response: Mapping[str, An
13941415
# would be better to have that in types though
13951416
directory = os.path.normpath(cast(str, directory))
13961417

1397-
videos_phrase = "" if skip_videos else " and videos"
1418+
if skip_photos or skip_videos:
1419+
photo_video_phrase = "photos" if skip_videos else "videos"
1420+
else:
1421+
photo_video_phrase = "photos and videos"
13981422
album_phrase = f" from album {album}" if album else ""
1399-
logger.debug(f"Looking up all photos{videos_phrase}{album_phrase}...")
1423+
logger.debug(f"Looking up all {photo_video_phrase}{album_phrase}...")
14001424

14011425
session_exception_handler = partial(session_error_handle_builder, logger, icloud)
14021426
internal_error_handler = partial(internal_error_handle_builder, logger)
@@ -1439,21 +1463,25 @@ def append_response(captured: List[Mapping[str, Any]], response: Mapping[str, An
14391463

14401464
if photos_count is not None:
14411465
plural_suffix = "" if photos_count == 1 else "s"
1442-
video_suffix = ""
1443-
photos_count_str = "the first" if photos_count == 1 else photos_count
1466+
photos_count_str = "the first" if photos_count == 1 else str(photos_count)
14441467

1445-
if not skip_videos:
1446-
video_suffix = " or video" if photos_count == 1 else " and videos"
1468+
if skip_photos or skip_videos:
1469+
photo_video_phrase = ("photo" if skip_videos else "video") + plural_suffix
1470+
else:
1471+
photo_video_phrase = (
1472+
"photo or video" if photos_count == 1 else "photos and videos"
1473+
)
14471474
else:
14481475
photos_count_str = "???"
1449-
plural_suffix = "s"
1450-
video_suffix = " and videos" if not skip_videos else ""
1476+
if skip_photos or skip_videos:
1477+
photo_video_phrase = "photos" if skip_videos else "videos"
1478+
else:
1479+
photo_video_phrase = "photos and videos"
14511480
logger.info(
1452-
("Downloading %s %s" + " photo%s%s to %s ..."),
1481+
("Downloading %s %s %s to %s ..."),
14531482
photos_count_str,
14541483
",".join([_s.value for _s in primary_sizes]),
1455-
plural_suffix,
1456-
video_suffix,
1484+
photo_video_phrase,
14571485
directory,
14581486
)
14591487

@@ -1537,10 +1565,13 @@ def should_break(counter: Counter) -> bool:
15371565
logger.info("Iteration was cancelled")
15381566
status_exchange.get_progress().photos_last_message = "Iteration was cancelled"
15391567
else:
1540-
logger.info("All photos have been downloaded")
1541-
status_exchange.get_progress().photos_last_message = (
1542-
"All photos have been downloaded"
1543-
)
1568+
if skip_photos or skip_videos:
1569+
photo_video_phrase = "photos" if skip_videos else "videos"
1570+
else:
1571+
photo_video_phrase = "photos and videos"
1572+
message = f"All {photo_video_phrase} have been downloaded"
1573+
logger.info(message)
1574+
status_exchange.get_progress().photos_last_message = message
15441575
status_exchange.get_progress().reset()
15451576

15461577
if auto_delete:

tests/test_autodelete_photos.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,9 @@ def astimezone(self, _tz: (Any | None) = None) -> NoReturn:
9595
"INFO Deleted IMG_3589.JPG",
9696
self._caplog.text,
9797
)
98-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
98+
self.assertIn(
99+
"INFO All photos and videos have been downloaded", self._caplog.text
100+
)
99101

100102
# check files
101103
for file_name in files:
@@ -129,7 +131,9 @@ def astimezone(self, _tz: (Any | None) = None) -> NoReturn:
129131
f"INFO Downloading 0 original photos and videos to {data_dir} ...",
130132
self._caplog.text,
131133
)
132-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
134+
self.assertIn(
135+
"INFO All photos and videos have been downloaded", self._caplog.text
136+
)
133137
self.assertIn(
134138
"INFO Deleting any files found in 'Recently Deleted'...",
135139
self._caplog.text,
@@ -198,7 +202,7 @@ def test_download_autodelete_photos(self) -> None:
198202
"INFO Deleted IMG_3589.JPG",
199203
self._caplog.text,
200204
)
201-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
205+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
202206

203207
# check files
204208
for file_name in files:
@@ -232,7 +236,7 @@ def test_download_autodelete_photos(self) -> None:
232236
f"INFO Downloading 0 original photos and videos to {data_dir} ...",
233237
self._caplog.text,
234238
)
235-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
239+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
236240
self.assertIn(
237241
"INFO Deleting any files found in 'Recently Deleted'...",
238242
self._caplog.text,

tests/test_download_live_photos.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def test_skip_existing_downloads_for_live_photos(self) -> None:
6363
],
6464
)
6565

66-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
66+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
6767
assert result.exit_code == 0
6868

6969
def test_skip_existing_live_photodownloads(self) -> None:
@@ -105,7 +105,7 @@ def test_skip_existing_live_photodownloads(self) -> None:
105105
f"INFO Downloading 3 original photos and videos to {data_dir} ...",
106106
self._caplog.text,
107107
)
108-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
108+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
109109
assert result.exit_code == 0
110110

111111
def test_skip_existing_live_photo_print_filenames(self) -> None:

tests/test_download_live_photos_id.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def test_skip_existing_downloads_for_live_photos_name_id7(self) -> None:
5151
],
5252
)
5353

54-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
54+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
5555
assert result.exit_code == 0
5656

5757
def test_skip_existing_live_photodownloads_name_id7(self) -> None:
@@ -93,7 +93,7 @@ def test_skip_existing_live_photodownloads_name_id7(self) -> None:
9393
f"INFO Downloading 3 original photos and videos to {data_dir} ...",
9494
self._caplog.text,
9595
)
96-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
96+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
9797
assert result.exit_code == 0
9898

9999
def test_skip_existing_live_photo_print_filenames_name_id7(self) -> None:

tests/test_download_photos.py

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def mocked_download(pa: PhotoAsset, _url: str) -> Response:
194194
f"DEBUG Setting EXIF timestamp for {os.path.join(data_dir, os.path.normpath('2018/07/31/IMG_7409.JPG'))}: {expectedDatetime}",
195195
self._caplog.text,
196196
)
197-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
197+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
198198

199199
def test_download_photos_and_get_exif_exceptions(self) -> None:
200200
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
@@ -291,7 +291,7 @@ def test_skip_existing_downloads(self) -> None:
291291
f"DEBUG {os.path.join(data_dir, os.path.normpath('2018/07/31/IMG_7409.MOV'))} already exists",
292292
self._caplog.text,
293293
)
294-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
294+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
295295

296296
def test_until_found(self) -> None:
297297
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
@@ -660,7 +660,7 @@ def test_missing_size(self) -> None:
660660
f"Errors for {filename} size {size}",
661661
)
662662

663-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
663+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
664664
self.assertEqual(result.exit_code, 0, "Exit code")
665665

666666
def test_size_fallback_to_original(self) -> None:
@@ -713,7 +713,9 @@ def test_size_fallback_to_original(self) -> None:
713713
f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('2018/07/31/IMG_7409.JPG'))}",
714714
self._caplog.text,
715715
)
716-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
716+
self.assertIn(
717+
"INFO All photos and videos have been downloaded", self._caplog.text
718+
)
717719
dp_patched.assert_called_once_with(
718720
ANY,
719721
False,
@@ -773,7 +775,9 @@ def test_force_size(self) -> None:
773775
"ERROR thumb size does not exist for IMG_7409.JPG. Skipping...",
774776
self._caplog.text,
775777
)
776-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
778+
self.assertIn(
779+
"INFO All photos and videos have been downloaded", self._caplog.text
780+
)
777781
dp_patched.assert_not_called()
778782

779783
assert result.exit_code == 0
@@ -829,7 +833,9 @@ def test_download_two_sizes_with_force_size(self) -> None:
829833
"ERROR medium size does not exist for IMG_7409.JPG. Skipping...",
830834
self._caplog.text,
831835
)
832-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
836+
self.assertIn(
837+
"INFO All photos and videos have been downloaded", self._caplog.text
838+
)
833839
dp_patched.assert_called_once_with(
834840
ANY,
835841
False,
@@ -893,7 +899,7 @@ def astimezone(self, _tz: (Any | None) = None) -> NoReturn:
893899
f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('2018/01/01/IMG_7409.JPG'))}",
894900
self._caplog.text,
895901
)
896-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
902+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
897903
assert result.exit_code == 0
898904

899905
@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
@@ -951,7 +957,7 @@ def test_creation_date_without_century(self) -> None:
951957
f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('5/01/01/IMG_7409.JPG'))}",
952958
self._caplog.text,
953959
)
954-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
960+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
955961
assert result.exit_code == 0
956962

957963
def test_creation_date_prior_1970(self) -> None:
@@ -999,7 +1005,7 @@ def test_creation_date_prior_1970(self) -> None:
9991005
f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('1965/01/01/IMG_7409.JPG'))}",
10001006
self._caplog.text,
10011007
)
1002-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
1008+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
10031009
assert result.exit_code == 0
10041010

10051011
def test_missing_item_type(self) -> None:
@@ -1310,7 +1316,9 @@ def mocked_download(pa: PhotoAsset, _url: str) -> Response:
13101316
f"INFO Downloading the first original photo or video to {data_dir} ...",
13111317
self._caplog.text,
13121318
)
1313-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
1319+
self.assertIn(
1320+
"INFO All photos and videos have been downloaded", self._caplog.text
1321+
)
13141322
assert result.exit_code == 0
13151323

13161324
def test_download_one_recent_live_photo_chinese(self) -> None:
@@ -1367,7 +1375,9 @@ def mocked_download(pa: PhotoAsset, _url: str) -> Response:
13671375
f"INFO Downloading the first original photo or video to {data_dir} ...",
13681376
self._caplog.text,
13691377
)
1370-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
1378+
self.assertIn(
1379+
"INFO All photos and videos have been downloaded", self._caplog.text
1380+
)
13711381
assert result.exit_code == 0
13721382

13731383
def test_download_and_delete_after(self) -> None:
@@ -2343,7 +2353,7 @@ def test_download_from_shared_library(self) -> None:
23432353
f"INFO Downloading the first original photo or video to {data_dir} ...",
23442354
self._caplog.text,
23452355
)
2346-
self.assertIn("INFO All photos have been downloaded", self._caplog.text)
2356+
self.assertIn("INFO All photos and videos have been downloaded", self._caplog.text)
23472357

23482358
def test_download_and_skip_old(self) -> None:
23492359
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])

0 commit comments

Comments
 (0)