Skip to content

Commit

Permalink
merged the (old) shared library support into the new structure (#678)
Browse files Browse the repository at this point in the history
  • Loading branch information
fver authored Sep 26, 2023
1 parent 0525fcf commit afd6f8a
Show file tree
Hide file tree
Showing 18 changed files with 2,544 additions and 465 deletions.
11 changes: 9 additions & 2 deletions src/icloudpd/autodelete.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,27 @@ def delete_file(logger, path) -> bool:
logger.info("Deleted %s", path)
return True


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

def autodelete_photos(logger, dry_run, icloud, folder_structure, directory):

def autodelete_photos(
logger,
dry_run,
library_object,
folder_structure,
directory):
"""
Scans the "Recently Deleted" folder and deletes any matching files
from the download directory.
(I.e. If you delete a photo on your phone, it's also deleted on your computer.)
"""
logger.info("Deleting any files found in 'Recently Deleted'...")

recently_deleted = icloud.photos.albums["Recently Deleted"]
recently_deleted = library_object.albums["Recently Deleted"]

for media in recently_deleted:
try:
Expand Down
312 changes: 172 additions & 140 deletions src/icloudpd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@
help="Lists the available albums",
is_flag=True,
)
@click.option(
"--library",
help="Library to download (default: Personal Library)",
metavar="<library>",
default="PrimarySync",
)
@click.option(
"--list-libraries",
help="Lists the available libraries",
is_flag=True,
)
@click.option(
"--skip-videos",
help="Don't download any videos (default: Download all photos and videos)",
Expand Down Expand Up @@ -122,12 +133,13 @@
+ "(Does not download or delete any files.)",
is_flag=True,
)
@click.option("--folder-structure",
help="Folder structure (default: {:%Y/%m/%d}). "
"If set to 'none' all photos will just be placed into the download directory",
metavar="<folder_structure>",
default="{:%Y/%m/%d}",
)
@click.option(
"--folder-structure",
help="Folder structure (default: {:%Y/%m/%d}). "
"If set to 'none' all photos will just be placed into the download directory",
metavar="<folder_structure>",
default="{:%Y/%m/%d}",
)
@click.option(
"--set-exif-datetime",
help="Write the DateTimeOriginal exif tag from file creation date, " +
Expand Down Expand Up @@ -235,6 +247,8 @@ def main(
until_found,
album,
list_albums,
library,
list_libraries,
skip_videos,
skip_live_photos,
force_size,
Expand Down Expand Up @@ -281,8 +295,8 @@ def main(
with logging_redirect_tqdm():

# check required directory param only if not list albums
if not list_albums and not directory:
print('--directory or --list-albums are required')
if not list_albums and not list_libraries and not directory:
print('--directory, --list-libraries or --list-albums are required')
sys.exit(2)

if auto_delete and delete_after_download:
Expand Down Expand Up @@ -318,6 +332,8 @@ def main(
until_found,
album,
list_albums,
library,
list_libraries,
skip_videos,
auto_delete,
only_print_filenames,
Expand Down Expand Up @@ -691,6 +707,8 @@ def core(
until_found,
album,
list_albums,
library,
list_libraries,
skip_videos,
auto_delete,
only_print_filenames,
Expand Down Expand Up @@ -742,143 +760,157 @@ def core(

download_photo = downloader(icloud)

while True:
# Access to the selected library. Defaults to the primary photos object.
library_object = icloud.photos

# Default album is "All Photos", so this is the same as
# calling `icloud.photos.all`.
# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
# case exit.
try:
photos = icloud.photos.albums[album]
except PyiCloudAPIResponseError as err:
# For later: come up with a nicer message to the user. For now take the
# exception text
print(err)
return 1

if list_albums:
albums_dict = icloud.photos.albums
albums = albums_dict.values() # pragma: no cover
album_titles = [str(a) for a in albums]
print(*album_titles, sep="\n")
return 0

directory = os.path.normpath(directory)

videos_phrase = "" if skip_videos else " and videos"
logger.debug(
"Looking up all photos%s from album %s...",
videos_phrase,
album
)
if list_libraries:
libraries_dict = icloud.photos.libraries
library_names = libraries_dict.keys()
print(*library_names, sep="\n")

session_exception_handler = session_error_handle_builder(
logger, icloud)
internal_error_handler = internal_error_handle_builder(logger)

error_handler = compose_handlers([session_exception_handler, internal_error_handler
])

photos.exception_handler = error_handler

photos_count = len(photos)

# Optional: Only download the x most recent photos.
if recent is not None:
photos_count = recent
photos = itertools.islice(photos, recent)

tqdm_kwargs = {"total": photos_count}

if until_found is not None:
del tqdm_kwargs["total"]
photos_count = "???"
# ensure photos iterator doesn't have a known length
photos = (p for p in photos)

# Use only ASCII characters in progress bar
tqdm_kwargs["ascii"] = True

tqdm_kwargs["leave"] = False
tqdm_kwargs["dynamic_ncols"] = True

# Skip the one-line progress bar if we're only printing the filenames,
# or if the progress bar is explicitly disabled,
# or if this is not a terminal (e.g. cron or piping output to file)
skip_bar = not os.environ.get("FORCE_TQDM") and (
only_print_filenames or no_progress_bar or not sys.stdout.isatty())
if skip_bar:
photos_enumerator = photos
# logger.set_tqdm(None)
else:
photos_enumerator = tqdm(photos, **tqdm_kwargs)
# logger.set_tqdm(photos_enumerator)

plural_suffix = "" if photos_count == 1 else "s"
video_suffix = ""
photos_count_str = "the first" if photos_count == 1 else photos_count
if not skip_videos:
video_suffix = " or video" if photos_count == 1 else " and videos"
logger.info(
("Downloading %s %s" +
" photo%s%s to %s ..."),
photos_count_str,
size,
plural_suffix,
video_suffix,
directory
)
else:
while True:
# Default album is "All Photos", so this is the same as
# calling `icloud.photos.all`.
# After 6 or 7 runs within 1h Apple blocks the API for some time. In that
# case exit.
try:
if library:
try:
library_object = icloud.photos.libraries[library]
except KeyError:
logger.error("Unknown library: %s", library)
return 1
photos = library_object.albums[album]
except PyiCloudAPIResponseError as err:
# For later: come up with a nicer message to the user. For now take the
# exception text
logger.error("error?? %s", err)
return 1

if list_albums:
print("Albums:")
albums_dict = library_object.albums
albums = albums_dict.values() # pragma: no cover
album_titles = [str(a) for a in albums]
print(*album_titles, sep="\n")
return 0
directory = os.path.normpath(directory)

videos_phrase = "" if skip_videos else " and videos"
logger.debug(
"Looking up all photos%s from album %s...",
videos_phrase,
album
)

consecutive_files_found = Counter(0)
session_exception_handler = session_error_handle_builder(
logger, icloud)
internal_error_handler = internal_error_handle_builder(logger)

error_handler = compose_handlers([session_exception_handler, internal_error_handler
])

photos.exception_handler = error_handler

photos_count = len(photos)

# Optional: Only download the x most recent photos.
if recent is not None:
photos_count = recent
photos = itertools.islice(photos, recent)

tqdm_kwargs = {"total": photos_count}

if until_found is not None:
del tqdm_kwargs["total"]
photos_count = "???"
# ensure photos iterator doesn't have a known length
photos = (p for p in photos)

# Use only ASCII characters in progress bar
tqdm_kwargs["ascii"] = True

tqdm_kwargs["leave"] = False
tqdm_kwargs["dynamic_ncols"] = True

# Skip the one-line progress bar if we're only printing the filenames,
# or if the progress bar is explicitly disabled,
# or if this is not a terminal (e.g. cron or piping output to file)
skip_bar = not os.environ.get("FORCE_TQDM") and (
only_print_filenames or no_progress_bar or not sys.stdout.isatty())
if skip_bar:
photos_enumerator = photos
# logger.set_tqdm(None)
else:
photos_enumerator = tqdm(photos, **tqdm_kwargs)
# logger.set_tqdm(photos_enumerator)

plural_suffix = "" if photos_count == 1 else "s"
video_suffix = ""
photos_count_str = "the first" if photos_count == 1 else photos_count
if not skip_videos:
video_suffix = " or video" if photos_count == 1 else " and videos"
logger.info(
("Downloading %s %s" +
" photo%s%s to %s ..."),
photos_count_str,
size,
plural_suffix,
video_suffix,
directory
)

def should_break(counter):
"""Exit if until_found condition is reached"""
return until_found is not None and counter.value() >= until_found
consecutive_files_found = Counter(0)

photos_iterator = iter(photos_enumerator)
while True:
try:
if should_break(consecutive_files_found):
logger.info(
"Found %s consecutive previously downloaded photos. Exiting",
until_found
)
def should_break(counter):
"""Exit if until_found condition is reached"""
return until_found is not None and counter.value() >= until_found

photos_iterator = iter(photos_enumerator)
while True:
try:
if should_break(consecutive_files_found):
logger.info(
"Found %s consecutive previously downloaded photos. Exiting",
until_found
)
break
item = next(photos_iterator)
if download_photo(
consecutive_files_found,
item) and delete_after_download:

def delete_cmd():
delete_local = delete_photo_dry_run if dry_run else delete_photo
delete_local(logger, icloud, item)

retrier(delete_cmd, error_handler)

except StopIteration:
break
item = next(photos_iterator)
if download_photo(
consecutive_files_found,
item) and delete_after_download:

def delete_cmd():
delete_local = delete_photo_dry_run if dry_run else delete_photo
delete_local(logger, icloud, item)

retrier(delete_cmd, error_handler)

except StopIteration:
break

if only_print_filenames:
return 0

logger.info("All photos have been downloaded")

if auto_delete:
autodelete_photos(logger, dry_run, icloud,
folder_structure, directory)

if watch_interval: # pragma: no cover
logger.info(f"Waiting for {watch_interval} sec...")
interval = range(1, watch_interval)
for _ in interval if skip_bar else tqdm(
interval,
desc="Waiting...",
ascii=True,
leave=False,
dynamic_ncols=True
):
time.sleep(1)
else:
break

if only_print_filenames:
return 0

logger.info("All photos have been downloaded")

if auto_delete:
autodelete_photos(logger, dry_run, library_object,
folder_structure, directory)

if watch_interval: # pragma: no cover
logger.info(f"Waiting for {watch_interval} sec...")
interval = range(1, watch_interval)
for _ in interval if skip_bar else tqdm(
interval,
desc="Waiting...",
ascii=True,
leave=False,
dynamic_ncols=True
):
time.sleep(1)
else:
break # pragma: no cover

return 0
Loading

0 comments on commit afd6f8a

Please sign in to comment.