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
26 changes: 3 additions & 23 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,29 +25,9 @@ EXPOSE 8080
WORKDIR /app
RUN chmod +x /app/icloud /app/icloudpd

# Use a shell script to allow command selection
COPY <<EOF /app/entrypoint.sh
#!/bin/sh
# If first argument is 'icloud' or 'icloudpd', run the corresponding binary
case "\$1" in
icloud)
shift
exec /app/icloud "\$@"
;;
icloudpd)
shift
exec /app/icloudpd "\$@"
;;
*)
echo "Error: You must specify either 'icloud' or 'icloudpd' as the first argument."
echo "Usage: docker run <image> icloudpd [options]"
echo " or: docker run <image> icloud [options]"
exit 1
;;
esac
EOF

# Use a shell script to allow command selection and environment variable conversion
COPY entrypoint-wrapper.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh

# Default entrypoint allows command selection
# Default entrypoint allows command selection and env var conversion
ENTRYPOINT ["/app/entrypoint.sh"]
25 changes: 25 additions & 0 deletions Dockerfile.build-simple
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Simplified build Dockerfile that doesn't require BuildKit
FROM python:3.13-alpine3.19
WORKDIR /app
RUN apk update && \
apk add git curl binutils gcc libc-dev libffi-dev zlib-dev openssl-dev tzdata bash patchelf python3-dev musl-dev pkgconfig cargo
COPY LICENSE.md .
COPY README_PYPI.md .
COPY requirements-pip.txt .
COPY scripts scripts/
COPY binary_dist binary_dist/
COPY pyproject.toml .
COPY src src/
RUN python3 -m venv .venv && \
. .venv/bin/activate && \
python3 -m pip install --disable-pip-version-check -r requirements-pip.txt && \
pip3 install --disable-pip-version-check . --group dev --group devlinux
RUN . .venv/bin/activate && \
scripts/build_bin1 icloudpd && \
scripts/build_bin1 icloud
# Copy binaries to a known location
RUN mkdir -p /output && \
cp /app/dist/icloudpd-*-linux-musl-amd64 /output/icloudpd && \
cp /app/dist/icloud-*-linux-musl-amd64 /output/icloud && \
chmod +x /output/icloudpd /output/icloud

40 changes: 40 additions & 0 deletions Dockerfile.github
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Dockerfile para construir desde GitHub
# Clona el repositorio y construye los binarios
FROM python:3.13-alpine3.19 AS builder
WORKDIR /app

# Instalar git y dependencias de compilación
RUN apk update && \
apk add git curl binutils gcc libc-dev libffi-dev zlib-dev openssl-dev tzdata bash patchelf python3-dev musl-dev pkgconfig cargo

# Clonar el repositorio desde GitHub
# Usa tu fork o el repositorio oficial según prefieras
ARG GIT_REPO=https://github.com/jibanez-staticduo/icloud_photos_downloader.git
ARG GIT_BRANCH=master
RUN git clone --depth 1 --branch ${GIT_BRANCH} ${GIT_REPO} /app

# Construir los binarios
RUN python3 -m venv .venv && \
. .venv/bin/activate && \
python3 -m pip install --disable-pip-version-check -r requirements-pip.txt && \
pip3 install --disable-pip-version-check . --group dev --group devlinux
RUN . .venv/bin/activate && \
scripts/build_bin1 icloudpd && \
scripts/build_bin1 icloud

# Imagen final
FROM alpine:3.18
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"
RUN apk update && apk add --no-cache tzdata musl-locales musl-locales-lang
WORKDIR /app
COPY --from=builder /app/dist/icloud icloud
COPY --from=builder /app/dist/icloudpd icloudpd
RUN chmod +x /app/icloud /app/icloudpd

# Copiar el entrypoint wrapper desde el repositorio clonado
COPY --from=builder /app/entrypoint-wrapper.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENV TZ=UTC
EXPOSE 8080
ENTRYPOINT ["/app/entrypoint.sh"]

35 changes: 35 additions & 0 deletions Dockerfile.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Dockerfile para construir desde el código fuente local
# Construye los binarios y luego crea la imagen final en un solo paso
FROM python:3.13-alpine3.19 AS builder
WORKDIR /app
RUN apk update && \
apk add git curl binutils gcc libc-dev libffi-dev zlib-dev openssl-dev tzdata bash patchelf python3-dev musl-dev pkgconfig cargo
COPY LICENSE.md .
COPY README_PYPI.md .
COPY requirements-pip.txt .
COPY scripts scripts/
COPY binary_dist binary_dist/
COPY pyproject.toml .
COPY src src/
RUN python3 -m venv .venv && \
. .venv/bin/activate && \
python3 -m pip install --disable-pip-version-check -r requirements-pip.txt && \
pip3 install --disable-pip-version-check . --group dev --group devlinux
RUN . .venv/bin/activate && \
scripts/build_bin1 icloudpd && \
scripts/build_bin1 icloud

# Imagen final
FROM alpine:3.18
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"
RUN apk update && apk add --no-cache tzdata musl-locales musl-locales-lang
WORKDIR /app
COPY --from=builder /app/dist/icloud icloud
COPY --from=builder /app/dist/icloudpd icloudpd
RUN chmod +x /app/icloud /app/icloudpd
COPY entrypoint-wrapper.sh /app/entrypoint.sh
RUN chmod +x /app/entrypoint.sh
ENV TZ=UTC
EXPOSE 8080
ENTRYPOINT ["/app/entrypoint.sh"]

10 changes: 10 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
The MIT License (MIT)

Copyright (c) 2016 Nathan Broadbent
Copyright (c) 2016-2025 The iCloud Photo Downloader Authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand All @@ -19,3 +20,12 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

---

This is a fork of the original iCloud Photos Downloader project:
https://github.com/icloud-photos-downloader/icloud_photos_downloader

This fork includes additional features such as Telegram bot integration for
remote control and authentication. All modifications are also licensed under
the MIT License.
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
# iCloud Photos Downloader [![Quality Checks](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workflows/Quality%20Checks/badge.svg)](https://github.com/icloud-photos-downloader/icloud_photos_downloader/actions/workflows/quality-checks.yml) [![Build and Package](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workflows/Produce%20Artifacts/badge.svg)](https://github.com/icloud-photos-downloader/icloud_photos_downloader/actions/workflows/produce-artifacts.yml) [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
# iCloud Photos Downloader [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.md)

> **Note:** This is a fork of the original [iCloud Photos Downloader](https://github.com/icloud-photos-downloader/icloud_photos_downloader) project with additional features including Telegram bot integration for remote control and authentication.

- A command-line tool to download all your iCloud photos.
- Works on Linux, Windows, and macOS; laptop, desktop, and NAS
- Available as an executable for direct downloading and through package managers/ecosystems ([Docker](https://icloud-photos-downloader.github.io/icloud_photos_downloader/install.html#docker), [PyPI](https://icloud-photos-downloader.github.io/icloud_photos_downloader/install.html#pypi), [AUR](https://icloud-photos-downloader.github.io/icloud_photos_downloader/install.html#aur), [npm](https://icloud-photos-downloader.github.io/icloud_photos_downloader/install.html#npm))
- Developed and maintained by volunteers (we are always looking for [help](CONTRIBUTING.md)).
- **Additional features in this fork:**
- Telegram bot integration for remote control (`/sync`, `/syncall`, `/stop`, `/status`, `/auth` commands)
- Telegram-based MFA authentication (no SSH required for cookie renewal)
- Automatic authentication expiration detection and notifications
- Based on the original project developed and maintained by volunteers (we are always looking for [help](CONTRIBUTING.md)).

See [Documentation](https://icloud-photos-downloader.github.io/icloud_photos_downloader/) for more details. Also, check [Issues](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues)

Expand Down
133 changes: 133 additions & 0 deletions entrypoint-wrapper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/bin/sh
# Wrapper script to convert environment variables to icloudpd command line arguments

ARGS=""

# Username (required)
if [ -n "$apple_id" ]; then
ARGS="$ARGS --username $apple_id"
elif [ -n "$APPLE_ID" ]; then
ARGS="$ARGS --username $APPLE_ID"
fi

# Directory (required)
if [ -n "$download_path" ]; then
ARGS="$ARGS --directory $download_path"
elif [ -n "$DOWNLOAD_PATH" ]; then
ARGS="$ARGS --directory $DOWNLOAD_PATH"
fi

# Watch interval
if [ -n "$synchronisation_interval" ]; then
ARGS="$ARGS --watch-with-interval $synchronisation_interval"
elif [ -n "$SYNCHRONISATION_INTERVAL" ]; then
ARGS="$ARGS --watch-with-interval $SYNCHRONISATION_INTERVAL"
fi

# Folder structure
if [ -n "$folder_structure" ]; then
ARGS="$ARGS --folder-structure \"$folder_structure\""
elif [ -n "$FOLDER_STRUCTURE" ]; then
ARGS="$ARGS --folder-structure \"$FOLDER_STRUCTURE\""
fi

# Cookie directory (default to /config if mounted)
if [ -n "$cookie_directory" ]; then
ARGS="$ARGS --cookie-directory $cookie_directory"
elif [ -n "$COOKIE_DIRECTORY" ]; then
ARGS="$ARGS --cookie-directory $COOKIE_DIRECTORY"
elif [ -d "/config" ]; then
ARGS="$ARGS --cookie-directory /config"
fi

# Boolean flags
# Nota: --skip-check y --delete-empty-directories no existen en el repositorio oficial
# --skip-album tampoco existe, pero podemos usar --album para especificar qué descargar

[ "$auto_delete" = "true" ] && ARGS="$ARGS --auto-delete" || true
[ "$AUTO_DELETE" = "true" ] && ARGS="$ARGS --auto-delete" || true

# convert_heic_to_jpeg no está disponible en el repositorio oficial

[ "$skip_videos" = "true" ] && ARGS="$ARGS --skip-videos" || true
[ "$SKIP_VIDEOS" = "true" ] && ARGS="$ARGS --skip-videos" || true

[ "$skip_live_photos" = "true" ] && ARGS="$ARGS --skip-live-photos" || true
[ "$SKIP_LIVE_PHOTOS" = "true" ] && ARGS="$ARGS --skip-live-photos" || true

[ "$set_exif_datetime" = "true" ] && ARGS="$ARGS --set-exif-datetime" || true
[ "$SET_EXIF_DATETIME" = "true" ] && ARGS="$ARGS --set-exif-datetime" || true

[ "$keep_unicode" = "true" ] && ARGS="$ARGS --keep-unicode-in-filenames" || true
[ "$KEEP_UNICODE" = "true" ] && ARGS="$ARGS --keep-unicode-in-filenames" || true

# Album (para especificar qué descargar, no para saltar)
if [ -n "$photo_album" ]; then
ARGS="$ARGS --album $photo_album"
elif [ -n "$PHOTO_ALBUM" ]; then
ARGS="$ARGS --album $PHOTO_ALBUM"
fi

# Photo size
if [ -n "$photo_size" ]; then
ARGS="$ARGS --size $photo_size"
elif [ -n "$PHOTO_SIZE" ]; then
ARGS="$ARGS --size $PHOTO_SIZE"
fi

# Live photo size
if [ -n "$live_photo_size" ]; then
ARGS="$ARGS --live-photo-size $live_photo_size"
elif [ -n "$LIVE_PHOTO_SIZE" ]; then
ARGS="$ARGS --live-photo-size $LIVE_PHOTO_SIZE"
fi

# Recent photos
if [ -n "$recent_only" ] && [ "$recent_only" != "0" ]; then
ARGS="$ARGS --recent $recent_only"
elif [ -n "$RECENT_ONLY" ] && [ "$RECENT_ONLY" != "0" ]; then
ARGS="$ARGS --recent $RECENT_ONLY"
fi

# Library
if [ -n "$photo_library" ]; then
ARGS="$ARGS --library $photo_library"
elif [ -n "$PHOTO_LIBRARY" ]; then
ARGS="$ARGS --library $PHOTO_LIBRARY"
fi

# Log level
if [ -n "$debug_logging" ] && [ "$debug_logging" = "true" ]; then
ARGS="$ARGS --log-level debug"
elif [ -n "$DEBUG_LOGGING" ] && [ "$DEBUG_LOGGING" = "true" ]; then
ARGS="$ARGS --log-level debug"
fi

# Password (if provided)
if [ -n "$password" ]; then
ARGS="$ARGS --password \"$password\""
elif [ -n "$PASSWORD" ]; then
ARGS="$ARGS --password \"$PASSWORD\""
fi

# Auth only mode
[ "$auth_only" = "true" ] && ARGS="$ARGS --auth-only" || true
[ "$AUTH_ONLY" = "true" ] && ARGS="$ARGS --auth-only" || true

# If no arguments were provided, show help
if [ -z "$ARGS" ] && [ "$1" != "icloudpd" ] && [ "$1" != "icloud" ]; then
exec /app/icloudpd --help
exit 0
fi

# If first argument is 'icloud' or 'icloudpd', use it, otherwise default to icloudpd
if [ "$1" = "icloud" ] || [ "$1" = "icloudpd" ]; then
CMD="$1"
shift
# Combine remaining args with our generated args
exec /app/$CMD $ARGS "$@"
else
# Default to icloudpd with our generated args
exec /app/icloudpd $ARGS "$@"
fi

62 changes: 62 additions & 0 deletions src/icloudpd/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ def password_provider(username: str, valid_password: List[str]) -> str | None:
notificator()
if mfa_provider == MFAProvider.WEBUI:
request_2fa_web(icloud, logger, status_exchange)
elif mfa_provider == MFAProvider.TELEGRAM:
request_2fa_telegram(icloud, logger, status_exchange)
else:
request_2fa(icloud, logger)

Expand Down Expand Up @@ -281,3 +283,63 @@ def request_2fa_web(
)
else:
raise PyiCloudFailedMFAException("Failed to change status")


def request_2fa_telegram(
icloud: PyiCloudService, logger: logging.Logger, status_exchange: StatusExchange
) -> None:
"""Request two-factor authentication through Telegram."""
if not status_exchange.replace_status(Status.NO_INPUT_NEEDED, Status.NEED_MFA):
raise PyiCloudFailedMFAException(
f"Expected NO_INPUT_NEEDED, but got {status_exchange.get_status()}"
)

# Get telegram bot from status_exchange if available
telegram_bot = status_exchange.get_telegram_bot()
if telegram_bot:
username = status_exchange.get_current_user() or "user"
telegram_bot.request_auth_code(username)
else:
logger.warning("Telegram bot not available, falling back to console")
# Fallback to console if Telegram bot not available
request_2fa(icloud, logger)
return

# wait for input
while True:
status = status_exchange.get_status()
if status == Status.NEED_MFA:
time.sleep(1)
continue
else:
pass

if status_exchange.replace_status(Status.SUPPLIED_MFA, Status.CHECKING_MFA):
code = status_exchange.get_payload()
if not code:
raise PyiCloudFailedMFAException(
"Internal error: did not get code for SUPPLIED_MFA status"
)

if not icloud.validate_2fa_code(code):
if status_exchange.set_error("Failed to verify two-factor authentication code"):
# Reset waiting flag and request code again
if telegram_bot:
telegram_bot.request_auth_code(username)
continue
else:
raise PyiCloudFailedMFAException("Failed to change status of invalid code")
else:
status_exchange.replace_status(Status.CHECKING_MFA, Status.NO_INPUT_NEEDED) # done
if telegram_bot:
telegram_bot.send_message("✅ Authentication completed successfully")
logger.info(
"Great, you're all set up. The script can now be run without "
"user interaction until 2FA expires.\n"
"You can set up email notifications for when "
"the two-factor authentication expires.\n"
"(Use --help to view information about SMTP options.)"
)
break
else:
raise PyiCloudFailedMFAException("Failed to change status")
Loading