Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

merged the (old) shared library support into the new structure #678

Merged
merged 6 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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