Skip to content
Open
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ node_modules/
.cookies/

.claude/

*.har
17 changes: 16 additions & 1 deletion src/icloudpd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,13 +650,20 @@ def download_builder(

version = versions[download_size]
photo_filename = filename_builder(photo)
filename_override = filename_overrides.get(download_size)

if file_match_policy == FileMatchPolicy.NAME_ID7_VERSIONED and download_size in (AssetVersionSize.ADJUSTED, AssetVersionSize.ALTERNATIVE):
ext = os.path.splitext(photo.calculate_version_filename(version, download_size, lp_filename_generator))[1]
stem = os.path.splitext(photo_filename)[0]
filename_override = f"{stem}-{download_size.value}{ext}"

filename = calculate_version_filename(
photo_filename,
version,
download_size,
lp_filename_generator,
photo.item_type,
filename_overrides.get(download_size),
filename_override,
)

download_path = local_download_path(filename, download_dir)
Expand Down Expand Up @@ -1209,6 +1216,10 @@ def should_break(counter: Counter) -> bool:
PyiCloudConnectionErrorException,
) as error:
logger.info(error)
if isinstance(error, PyiCloudAPIResponseException) and "Invalid global session" in str(
error
):
continue
dump_responses(logger.debug, captured_responses)
# webui will display error and wait for password again
if (
Expand All @@ -1234,6 +1245,10 @@ def should_break(counter: Counter) -> bool:
logger.debug("Retrying...")
# these errors we can safely retry
continue
except OSError as error:
logger.error("IOError during file operations: %s", error)
dump_responses(logger.debug, captured_responses)
return 1
except Exception:
dump_responses(logger.debug, captured_responses)
raise
Expand Down
19 changes: 17 additions & 2 deletions src/icloudpd/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import argparse
import copy
import datetime
import os
import pathlib
import sys
from itertools import dropwhile
Expand Down Expand Up @@ -234,8 +235,8 @@ def add_options_for_user(parser: argparse.ArgumentParser) -> argparse.ArgumentPa
)
cloned.add_argument(
"--file-match-policy",
help="Policy to identify existing files and de-duplicate. `name-size-dedup-with-suffix` appends file size to de-duplicate. `name-id7` adds asset ID from iCloud to all filenames and does not de-duplicate. Default: %(default)s",
choices=["name-size-dedup-with-suffix", "name-id7"],
help="Policy to identify existing files and de-duplicate. `name-size-dedup-with-suffix` appends file size to de-duplicate. `name-id7` adds asset ID from iCloud to all filenames and does not de-duplicate. `name-id7-versioned` is similar to `name-id7`, but adds asset ID from iCloud even on `adjusted` and `alternative`. Default: %(default)s",
choices=["name-size-dedup-with-suffix", "name-id7", "name-id7-versioned"],
default="name-size-dedup-with-suffix",
type=lower,
)
Expand Down Expand Up @@ -605,6 +606,20 @@ def cli() -> int:
"--watch-with-interval is not compatible with --list-albums, --list-libraries, --only-print-filenames, and --auth-only"
)
return 2

# Validate that directories exist for configurations that need them
elif [
user_ns
for user_ns in user_nses
if user_ns.directory and not os.path.exists(user_ns.directory)
]:
invalid_dirs = [
user_ns.directory
for user_ns in user_nses
if user_ns.directory and not os.path.exists(user_ns.directory)
]
print(f"Directory does not exist: {invalid_dirs[0]}")
return 2
else:
return run_with_configs(global_ns, user_nses)

Expand Down
95 changes: 23 additions & 72 deletions src/icloudpd/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@
from tzlocal import get_localzone

# Import the constants object so that we can mock WAIT_SECONDS in tests
from icloudpd import constants
from pyicloud_ipd.asset_version import AssetVersion, calculate_version_filename
from pyicloud_ipd.base import PyiCloudService
from pyicloud_ipd.exceptions import PyiCloudAPIResponseException
from pyicloud_ipd.services.photos import PhotoAsset
from pyicloud_ipd.version_size import VersionSize

Expand Down Expand Up @@ -128,76 +126,29 @@ def download_media(
partial(download_response_to_path_dry_run, logger) if dry_run else download_response_to_path
)

retries = 0
while True:
try:
append_mode = os.path.exists(temp_download_path)
current_size = os.path.getsize(temp_download_path) if append_mode else 0
if append_mode:
logger.debug(f"Resuming downloading of {download_path} from {current_size}")

photo_response = photo.download(icloud.photos.session, version.url, current_size)
if photo_response.ok:
return download_local(
photo_response, temp_download_path, append_mode, download_path, photo.created
)
else:
# Use the standard original filename generator for error logging
from icloudpd.base import lp_filename_original as simple_lp_filename_generator

# Get the proper filename using filename_builder
base_filename = filename_builder(photo)
version_filename = calculate_version_filename(
base_filename, version, size, simple_lp_filename_generator, photo.item_type
)
logger.error(
"Could not find URL to download %s for size %s",
version_filename,
size.value,
)
break

except PyiCloudAPIResponseException as ex:
if "Invalid global session" in str(ex):
logger.error("Session error, re-authenticating...")
if retries > 0:
# If the first re-authentication attempt failed,
# start waiting a few seconds before retrying in case
# there are some issues with the Apple servers
time.sleep(constants.WAIT_SECONDS)

icloud.authenticate()
else:
# short circuiting 0 retries
if retries == constants.MAX_RETRIES:
break
# you end up here when p.e. throttling by Apple happens
wait_time = (retries + 1) * constants.WAIT_SECONDS
# Get the proper filename for error messages
error_filename = filename_builder(photo)
logger.error(
"Error downloading %s, retrying after %s seconds...", error_filename, wait_time
)
time.sleep(wait_time)

except OSError:
logger.error(
"IOError while writing file to %s. "
+ "You might have run out of disk space, or the file "
+ "might be too large for your OS. "
+ "Skipping this file...",
download_path,
)
break
retries = retries + 1
if retries >= constants.MAX_RETRIES:
break
if retries >= constants.MAX_RETRIES:
# Get the proper filename for error messages
error_filename = filename_builder(photo)
append_mode = os.path.exists(temp_download_path)
current_size = os.path.getsize(temp_download_path) if append_mode else 0
if append_mode:
logger.debug(f"Resuming downloading of {download_path} from {current_size}")

photo_response = photo.download(icloud.photos.session, version.url, current_size)
if photo_response.ok:
return download_local(
photo_response, temp_download_path, append_mode, download_path, photo.created
)
else:
# Use the standard original filename generator for error logging
from icloudpd.base import lp_filename_original as simple_lp_filename_generator

# Get the proper filename using filename_builder
base_filename = filename_builder(photo)
version_filename = calculate_version_filename(
base_filename, version, size, simple_lp_filename_generator, photo.item_type
)
logger.error(
"Could not download %s. Please try again later.",
error_filename,
"Could not find URL to download %s for size %s",
version_filename,
size.value,
)

return False
return False
Loading