|
93 | 93 | help="Lists the available albums", |
94 | 94 | is_flag=True, |
95 | 95 | ) |
| 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 | +) |
96 | 107 | @click.option( |
97 | 108 | "--skip-videos", |
98 | 109 | help="Don't download any videos (default: Download all photos and videos)", |
|
122 | 133 | + "(Does not download or delete any files.)", |
123 | 134 | is_flag=True, |
124 | 135 | ) |
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 | +) |
131 | 143 | @click.option( |
132 | 144 | "--set-exif-datetime", |
133 | 145 | help="Write the DateTimeOriginal exif tag from file creation date, " + |
@@ -235,6 +247,8 @@ def main( |
235 | 247 | until_found, |
236 | 248 | album, |
237 | 249 | list_albums, |
| 250 | + library, |
| 251 | + list_libraries, |
238 | 252 | skip_videos, |
239 | 253 | skip_live_photos, |
240 | 254 | force_size, |
@@ -281,8 +295,8 @@ def main( |
281 | 295 | with logging_redirect_tqdm(): |
282 | 296 |
|
283 | 297 | # 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') |
286 | 300 | sys.exit(2) |
287 | 301 |
|
288 | 302 | if auto_delete and delete_after_download: |
@@ -318,6 +332,8 @@ def main( |
318 | 332 | until_found, |
319 | 333 | album, |
320 | 334 | list_albums, |
| 335 | + library, |
| 336 | + list_libraries, |
321 | 337 | skip_videos, |
322 | 338 | auto_delete, |
323 | 339 | only_print_filenames, |
@@ -691,6 +707,8 @@ def core( |
691 | 707 | until_found, |
692 | 708 | album, |
693 | 709 | list_albums, |
| 710 | + library, |
| 711 | + list_libraries, |
694 | 712 | skip_videos, |
695 | 713 | auto_delete, |
696 | 714 | only_print_filenames, |
@@ -742,143 +760,157 @@ def core( |
742 | 760 |
|
743 | 761 | download_photo = downloader(icloud) |
744 | 762 |
|
745 | | - while True: |
| 763 | + # Access to the selected library. Defaults to the primary photos object. |
| 764 | + library_object = icloud.photos |
746 | 765 |
|
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") |
774 | 770 |
|
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 | + ) |
831 | 806 |
|
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 | + ) |
833 | 863 |
|
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) |
837 | 865 |
|
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: |
846 | 891 | 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 |
883 | 915 |
|
884 | 916 | return 0 |
0 commit comments