Skip to content

Commit 70d9b55

Browse files
authored
merged the (old) shared library support into the new structure (#678)
1 parent 023e90e commit 70d9b55

18 files changed

+2544
-465
lines changed

src/icloudpd/autodelete.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,27 @@ def delete_file(logger, path) -> bool:
1212
logger.info("Deleted %s", path)
1313
return True
1414

15+
1516
def delete_file_dry_run(logger, path) -> bool:
1617
""" Dry run deletion of files """
1718
logger.info("[DRY RUN] Would delete %s", path)
1819
return True
1920

20-
def autodelete_photos(logger, dry_run, icloud, folder_structure, directory):
21+
22+
def autodelete_photos(
23+
logger,
24+
dry_run,
25+
library_object,
26+
folder_structure,
27+
directory):
2128
"""
2229
Scans the "Recently Deleted" folder and deletes any matching files
2330
from the download directory.
2431
(I.e. If you delete a photo on your phone, it's also deleted on your computer.)
2532
"""
2633
logger.info("Deleting any files found in 'Recently Deleted'...")
2734

28-
recently_deleted = icloud.photos.albums["Recently Deleted"]
35+
recently_deleted = library_object.albums["Recently Deleted"]
2936

3037
for media in recently_deleted:
3138
try:

src/icloudpd/base.py

Lines changed: 172 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,17 @@
9393
help="Lists the available albums",
9494
is_flag=True,
9595
)
96+
@click.option(
97+
"--library",
98+
help="Library to download (default: Personal Library)",
99+
metavar="<library>",
100+
default="PrimarySync",
101+
)
102+
@click.option(
103+
"--list-libraries",
104+
help="Lists the available libraries",
105+
is_flag=True,
106+
)
96107
@click.option(
97108
"--skip-videos",
98109
help="Don't download any videos (default: Download all photos and videos)",
@@ -122,12 +133,13 @@
122133
+ "(Does not download or delete any files.)",
123134
is_flag=True,
124135
)
125-
@click.option("--folder-structure",
126-
help="Folder structure (default: {:%Y/%m/%d}). "
127-
"If set to 'none' all photos will just be placed into the download directory",
128-
metavar="<folder_structure>",
129-
default="{:%Y/%m/%d}",
130-
)
136+
@click.option(
137+
"--folder-structure",
138+
help="Folder structure (default: {:%Y/%m/%d}). "
139+
"If set to 'none' all photos will just be placed into the download directory",
140+
metavar="<folder_structure>",
141+
default="{:%Y/%m/%d}",
142+
)
131143
@click.option(
132144
"--set-exif-datetime",
133145
help="Write the DateTimeOriginal exif tag from file creation date, " +
@@ -235,6 +247,8 @@ def main(
235247
until_found,
236248
album,
237249
list_albums,
250+
library,
251+
list_libraries,
238252
skip_videos,
239253
skip_live_photos,
240254
force_size,
@@ -281,8 +295,8 @@ def main(
281295
with logging_redirect_tqdm():
282296

283297
# check required directory param only if not list albums
284-
if not list_albums and not directory:
285-
print('--directory or --list-albums are required')
298+
if not list_albums and not list_libraries and not directory:
299+
print('--directory, --list-libraries or --list-albums are required')
286300
sys.exit(2)
287301

288302
if auto_delete and delete_after_download:
@@ -318,6 +332,8 @@ def main(
318332
until_found,
319333
album,
320334
list_albums,
335+
library,
336+
list_libraries,
321337
skip_videos,
322338
auto_delete,
323339
only_print_filenames,
@@ -691,6 +707,8 @@ def core(
691707
until_found,
692708
album,
693709
list_albums,
710+
library,
711+
list_libraries,
694712
skip_videos,
695713
auto_delete,
696714
only_print_filenames,
@@ -742,143 +760,157 @@ def core(
742760

743761
download_photo = downloader(icloud)
744762

745-
while True:
763+
# Access to the selected library. Defaults to the primary photos object.
764+
library_object = icloud.photos
746765

747-
# Default album is "All Photos", so this is the same as
748-
# calling `icloud.photos.all`.
749-
# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
750-
# case exit.
751-
try:
752-
photos = icloud.photos.albums[album]
753-
except PyiCloudAPIResponseError as err:
754-
# For later: come up with a nicer message to the user. For now take the
755-
# exception text
756-
print(err)
757-
return 1
758-
759-
if list_albums:
760-
albums_dict = icloud.photos.albums
761-
albums = albums_dict.values() # pragma: no cover
762-
album_titles = [str(a) for a in albums]
763-
print(*album_titles, sep="\n")
764-
return 0
765-
766-
directory = os.path.normpath(directory)
767-
768-
videos_phrase = "" if skip_videos else " and videos"
769-
logger.debug(
770-
"Looking up all photos%s from album %s...",
771-
videos_phrase,
772-
album
773-
)
766+
if list_libraries:
767+
libraries_dict = icloud.photos.libraries
768+
library_names = libraries_dict.keys()
769+
print(*library_names, sep="\n")
774770

775-
session_exception_handler = session_error_handle_builder(
776-
logger, icloud)
777-
internal_error_handler = internal_error_handle_builder(logger)
778-
779-
error_handler = compose_handlers([session_exception_handler, internal_error_handler
780-
])
781-
782-
photos.exception_handler = error_handler
783-
784-
photos_count = len(photos)
785-
786-
# Optional: Only download the x most recent photos.
787-
if recent is not None:
788-
photos_count = recent
789-
photos = itertools.islice(photos, recent)
790-
791-
tqdm_kwargs = {"total": photos_count}
792-
793-
if until_found is not None:
794-
del tqdm_kwargs["total"]
795-
photos_count = "???"
796-
# ensure photos iterator doesn't have a known length
797-
photos = (p for p in photos)
798-
799-
# Use only ASCII characters in progress bar
800-
tqdm_kwargs["ascii"] = True
801-
802-
tqdm_kwargs["leave"] = False
803-
tqdm_kwargs["dynamic_ncols"] = True
804-
805-
# Skip the one-line progress bar if we're only printing the filenames,
806-
# or if the progress bar is explicitly disabled,
807-
# or if this is not a terminal (e.g. cron or piping output to file)
808-
skip_bar = not os.environ.get("FORCE_TQDM") and (
809-
only_print_filenames or no_progress_bar or not sys.stdout.isatty())
810-
if skip_bar:
811-
photos_enumerator = photos
812-
# logger.set_tqdm(None)
813-
else:
814-
photos_enumerator = tqdm(photos, **tqdm_kwargs)
815-
# logger.set_tqdm(photos_enumerator)
816-
817-
plural_suffix = "" if photos_count == 1 else "s"
818-
video_suffix = ""
819-
photos_count_str = "the first" if photos_count == 1 else photos_count
820-
if not skip_videos:
821-
video_suffix = " or video" if photos_count == 1 else " and videos"
822-
logger.info(
823-
("Downloading %s %s" +
824-
" photo%s%s to %s ..."),
825-
photos_count_str,
826-
size,
827-
plural_suffix,
828-
video_suffix,
829-
directory
830-
)
771+
else:
772+
while True:
773+
# Default album is "All Photos", so this is the same as
774+
# calling `icloud.photos.all`.
775+
# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
776+
# case exit.
777+
try:
778+
if library:
779+
try:
780+
library_object = icloud.photos.libraries[library]
781+
except KeyError:
782+
logger.error("Unknown library: %s", library)
783+
return 1
784+
photos = library_object.albums[album]
785+
except PyiCloudAPIResponseError as err:
786+
# For later: come up with a nicer message to the user. For now take the
787+
# exception text
788+
logger.error("error?? %s", err)
789+
return 1
790+
791+
if list_albums:
792+
print("Albums:")
793+
albums_dict = library_object.albums
794+
albums = albums_dict.values() # pragma: no cover
795+
album_titles = [str(a) for a in albums]
796+
print(*album_titles, sep="\n")
797+
return 0
798+
directory = os.path.normpath(directory)
799+
800+
videos_phrase = "" if skip_videos else " and videos"
801+
logger.debug(
802+
"Looking up all photos%s from album %s...",
803+
videos_phrase,
804+
album
805+
)
831806

832-
consecutive_files_found = Counter(0)
807+
session_exception_handler = session_error_handle_builder(
808+
logger, icloud)
809+
internal_error_handler = internal_error_handle_builder(logger)
810+
811+
error_handler = compose_handlers([session_exception_handler, internal_error_handler
812+
])
813+
814+
photos.exception_handler = error_handler
815+
816+
photos_count = len(photos)
817+
818+
# Optional: Only download the x most recent photos.
819+
if recent is not None:
820+
photos_count = recent
821+
photos = itertools.islice(photos, recent)
822+
823+
tqdm_kwargs = {"total": photos_count}
824+
825+
if until_found is not None:
826+
del tqdm_kwargs["total"]
827+
photos_count = "???"
828+
# ensure photos iterator doesn't have a known length
829+
photos = (p for p in photos)
830+
831+
# Use only ASCII characters in progress bar
832+
tqdm_kwargs["ascii"] = True
833+
834+
tqdm_kwargs["leave"] = False
835+
tqdm_kwargs["dynamic_ncols"] = True
836+
837+
# Skip the one-line progress bar if we're only printing the filenames,
838+
# or if the progress bar is explicitly disabled,
839+
# or if this is not a terminal (e.g. cron or piping output to file)
840+
skip_bar = not os.environ.get("FORCE_TQDM") and (
841+
only_print_filenames or no_progress_bar or not sys.stdout.isatty())
842+
if skip_bar:
843+
photos_enumerator = photos
844+
# logger.set_tqdm(None)
845+
else:
846+
photos_enumerator = tqdm(photos, **tqdm_kwargs)
847+
# logger.set_tqdm(photos_enumerator)
848+
849+
plural_suffix = "" if photos_count == 1 else "s"
850+
video_suffix = ""
851+
photos_count_str = "the first" if photos_count == 1 else photos_count
852+
if not skip_videos:
853+
video_suffix = " or video" if photos_count == 1 else " and videos"
854+
logger.info(
855+
("Downloading %s %s" +
856+
" photo%s%s to %s ..."),
857+
photos_count_str,
858+
size,
859+
plural_suffix,
860+
video_suffix,
861+
directory
862+
)
833863

834-
def should_break(counter):
835-
"""Exit if until_found condition is reached"""
836-
return until_found is not None and counter.value() >= until_found
864+
consecutive_files_found = Counter(0)
837865

838-
photos_iterator = iter(photos_enumerator)
839-
while True:
840-
try:
841-
if should_break(consecutive_files_found):
842-
logger.info(
843-
"Found %s consecutive previously downloaded photos. Exiting",
844-
until_found
845-
)
866+
def should_break(counter):
867+
"""Exit if until_found condition is reached"""
868+
return until_found is not None and counter.value() >= until_found
869+
870+
photos_iterator = iter(photos_enumerator)
871+
while True:
872+
try:
873+
if should_break(consecutive_files_found):
874+
logger.info(
875+
"Found %s consecutive previously downloaded photos. Exiting",
876+
until_found
877+
)
878+
break
879+
item = next(photos_iterator)
880+
if download_photo(
881+
consecutive_files_found,
882+
item) and delete_after_download:
883+
884+
def delete_cmd():
885+
delete_local = delete_photo_dry_run if dry_run else delete_photo
886+
delete_local(logger, icloud, item)
887+
888+
retrier(delete_cmd, error_handler)
889+
890+
except StopIteration:
846891
break
847-
item = next(photos_iterator)
848-
if download_photo(
849-
consecutive_files_found,
850-
item) and delete_after_download:
851-
852-
def delete_cmd():
853-
delete_local = delete_photo_dry_run if dry_run else delete_photo
854-
delete_local(logger, icloud, item)
855-
856-
retrier(delete_cmd, error_handler)
857-
858-
except StopIteration:
859-
break
860-
861-
if only_print_filenames:
862-
return 0
863-
864-
logger.info("All photos have been downloaded")
865-
866-
if auto_delete:
867-
autodelete_photos(logger, dry_run, icloud,
868-
folder_structure, directory)
869-
870-
if watch_interval: # pragma: no cover
871-
logger.info(f"Waiting for {watch_interval} sec...")
872-
interval = range(1, watch_interval)
873-
for _ in interval if skip_bar else tqdm(
874-
interval,
875-
desc="Waiting...",
876-
ascii=True,
877-
leave=False,
878-
dynamic_ncols=True
879-
):
880-
time.sleep(1)
881-
else:
882-
break
892+
893+
if only_print_filenames:
894+
return 0
895+
896+
logger.info("All photos have been downloaded")
897+
898+
if auto_delete:
899+
autodelete_photos(logger, dry_run, library_object,
900+
folder_structure, directory)
901+
902+
if watch_interval: # pragma: no cover
903+
logger.info(f"Waiting for {watch_interval} sec...")
904+
interval = range(1, watch_interval)
905+
for _ in interval if skip_bar else tqdm(
906+
interval,
907+
desc="Waiting...",
908+
ascii=True,
909+
leave=False,
910+
dynamic_ncols=True
911+
):
912+
time.sleep(1)
913+
else:
914+
break # pragma: no cover
883915

884916
return 0

0 commit comments

Comments
 (0)