master 3a97872f9f44 cached
224 files
55.3 MB
9.3M tokens
674 symbols
1 requests
Copy disabled (too large) Download .txt
Showing preview only (37,005K chars total). Download the full file to get everything.
Repository: icloud-photos-downloader/icloud_photos_downloader
Branch: master
Commit: 3a97872f9f44
Files: 224
Total size: 55.3 MB

Directory structure:
gitextract_lkob7b_6/

├── .devcontainer/
│   ├── node/
│   │   └── devcontainer.json
│   └── python/
│       └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   ├── feature-request.md
│   │   └── generic.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-docs.yml
│       ├── build-package.yml
│       ├── codeql-analysis.yml
│       ├── compile-notes.yml
│       ├── create-release.yml
│       ├── extract-changelog.yml
│       ├── patch-version.yml
│       ├── produce-artifacts.yml
│       ├── publish-docs.yml
│       ├── publish.yml
│       ├── quality-checks.yml
│       └── refresh-docs.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.build
├── Dockerfile.build-doc
├── Dockerfile.build-musl
├── EXPERIMENTAL.md
├── LICENSE.md
├── README.md
├── README_AUR.md
├── README_DOCKER.md
├── README_NPM.md
├── README_PYPI.md
├── binary_dist/
│   ├── pyproject.toml
│   └── src/
│       ├── icloud/
│       │   ├── __init__.py
│       │   └── __main__.py
│       └── icloudpd/
│           ├── __init__.py
│           └── __main__.py
├── docs/
│   ├── authentication.md
│   ├── conf.py
│   ├── index.md
│   ├── install.md
│   ├── mode.md
│   ├── naming.md
│   ├── nas.md
│   ├── raw.md
│   ├── reference.md
│   ├── size.md
│   └── webui.md
├── examples/
│   └── cron_script.sh.example
├── help_current.txt
├── jq/
│   └── version.jq
├── npm/
│   ├── @icloudpd/
│   │   ├── darwin-arm64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── darwin-x64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-arm/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-arm64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-x64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   └── win32-x64/
│   │       ├── README.md
│   │       └── package.json
│   └── icloudpd/
│       ├── bin/
│       │   └── icloudpd.js
│       ├── package.json
│       └── preinstall.js
├── pyproject.toml
├── requirements-pip.txt
├── scripts/
│   ├── build
│   ├── build_bin1
│   ├── build_binary_dist_macos
│   ├── build_binary_dist_windows
│   ├── build_npm
│   ├── build_static
│   ├── build_whl
│   ├── clean
│   ├── clone_whl_version
│   ├── compile_compatibility.py
│   ├── compile_matrix.py
│   ├── compile_tzlc.py
│   ├── extract_releasenotes
│   ├── format
│   ├── get_version
│   ├── install_deps
│   ├── lint
│   ├── npx_optional
│   ├── npx_optional_touch
│   ├── patch_version
│   ├── publish_npm
│   ├── publish_pypi
│   ├── run_all_checks
│   ├── test
│   ├── test_python_versions.sh
│   ├── type_check
│   └── unpublish_npm
├── src/
│   ├── foundation/
│   │   ├── __init__.py
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   └── optional/
│   │   │       └── __init__.py
│   │   ├── http.py
│   │   ├── json.py
│   │   ├── predicates.py
│   │   ├── py.typed
│   │   ├── string.py
│   │   └── string_utils.py
│   ├── icloudpd/
│   │   ├── __init__.py
│   │   ├── authentication.py
│   │   ├── autodelete.py
│   │   ├── base.py
│   │   ├── cli.py
│   │   ├── config.py
│   │   ├── constants.py
│   │   ├── counter.py
│   │   ├── download.py
│   │   ├── email_notifications.py
│   │   ├── exif_datetime.py
│   │   ├── filename_policies.py
│   │   ├── log_level.py
│   │   ├── logger.py
│   │   ├── mfa_provider.py
│   │   ├── password_provider.py
│   │   ├── paths.py
│   │   ├── progress.py
│   │   ├── py.typed
│   │   ├── server/
│   │   │   ├── __init__.py
│   │   │   ├── static/
│   │   │   │   ├── js/
│   │   │   │   │   └── toast.js
│   │   │   │   ├── manifest.json
│   │   │   │   └── style/
│   │   │   │       └── style.css
│   │   │   └── templates/
│   │   │       ├── auth_error.html
│   │   │       ├── code.html
│   │   │       ├── code_submitted.html
│   │   │       ├── index.html
│   │   │       ├── no_input.html
│   │   │       ├── password.html
│   │   │       ├── password_submitted.html
│   │   │       └── status.html
│   │   ├── status.py
│   │   ├── string_helpers.py
│   │   └── xmp_sidecar.py
│   ├── pyicloud_ipd/
│   │   ├── __init__.py
│   │   ├── asset_version.py
│   │   ├── base.py
│   │   ├── cmdline.py
│   │   ├── exceptions.py
│   │   ├── file_match.py
│   │   ├── item_type.py
│   │   ├── live_photo_mov_filename_policy.py
│   │   ├── py.typed
│   │   ├── raw_policy.py
│   │   ├── services/
│   │   │   ├── __init__.py
│   │   │   └── photos.py
│   │   ├── session.py
│   │   ├── sms.py
│   │   ├── utils.py
│   │   └── version_size.py
│   └── starters/
│       ├── __init__.py
│       ├── icloud.py
│       └── icloudpd.py
└── tests/
    ├── __init__.py
    ├── cookie/
    │   ├── jdoegmailcom
    │   └── jdoegmailcom.session
    ├── data/
    │   └── parse_trusted_phone_numbers_payload_valid.html
    ├── helpers/
    │   └── __init__.py
    ├── test_authentication.py
    ├── test_autodelete_photos.py
    ├── test_cli.py
    ├── test_download_live_photos.py
    ├── test_download_live_photos_id.py
    ├── test_download_photos.py
    ├── test_download_photos_id.py
    ├── test_download_videos.py
    ├── test_email_notifications.py
    ├── test_filenames.py
    ├── test_folder_structure.py
    ├── test_http.py
    ├── test_issue_1220_only_print_filenames_dedup_bug.py
    ├── test_json_rules.py
    ├── test_keep_icloud_mode.py
    ├── test_listing_albums.py
    ├── test_listing_libraries.py
    ├── test_listing_recent_photos.py
    ├── test_logger.py
    ├── test_session_connection_errors.py
    ├── test_string_helpers.py
    ├── test_two_step_auth.py
    ├── test_xmp_sidecar.py
    └── vcr_cassettes/
        ├── 2fa_flow_invalid_code.yml
        ├── 2fa_flow_valid_code.yml
        ├── 2fa_flow_valid_code_zero_lead.yml
        ├── 2sa_flow_invalid_code.yml
        ├── 2sa_flow_valid_code.yml
        ├── auth_non_2fa.yml
        ├── auth_requires_2fa.yml
        ├── autodelete_photos.yml
        ├── autodelete_photos_heic.yml
        ├── download_autodelete_photos_part1.yml
        ├── download_autodelete_photos_part2.yml
        ├── download_live_photos.yml
        ├── failed_auth.yml
        ├── failed_auth_503.yml
        ├── fallback_raw_password.yml
        ├── listing_albums.yml
        ├── listing_photos.yml
        ├── listing_photos_bad_filename.yml
        ├── listing_photos_bad_filename_base64_encoding.yml
        ├── listing_photos_bad_filename_utf8_encoding.yml
        ├── listing_photos_chinese.yml
        ├── listing_photos_filename_string_encoding.yml
        ├── listing_photos_keep_icloud_recent_days.yml
        ├── listing_photos_missing_downloadUrl.yml
        ├── listing_photos_missing_filenameEnc.yml
        ├── listing_photos_missing_item_type.yml
        ├── listing_photos_missing_item_type_value.yml
        ├── listing_photos_no_delete.yml
        ├── listing_photos_raw.yml
        ├── listing_photos_raw_alt.yml
        ├── listing_photos_raw_alt_adj.yml
        ├── listing_photos_resume.yml
        ├── listing_photos_two_sizes.yml
        ├── listing_videos.yml
        └── successful_auth.yml

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/node/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node
{
	"name": "Node.js & TypeScript",
	// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
	"image": "mcr.microsoft.com/devcontainers/typescript-node:0-20"

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "yarn install",

	// Configure tool-specific properties.
	// "customizations": {},

	// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
	// "remoteUser": "root"
}


================================================
FILE: .devcontainer/python/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/python
{
	"name": "Python 3",
	// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
	"image": "mcr.microsoft.com/devcontainers/python:3.13",
	"features": {
		"ghcr.io/devcontainers/features/docker-in-docker:2": {}
	},
	"postCreateCommand": "sudo apt-get update && sudo apt-get install -y locales-all && (curl -fsSL https://claude.ai/install.sh | bash)"

	// Features to add to the dev container. More info: https://containers.dev/features.
	// "features": {},

	// Use 'forwardPorts' to make a list of ports inside the container available locally.
	// "forwardPorts": [],

	// Use 'postCreateCommand' to run commands after the container is created.
	// "postCreateCommand": "pip3 install --user -r requirements.txt",

	// Configure tool-specific properties.
	// "customizations": {},

	// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
	// "remoteUser": "root"
}


================================================
FILE: .dockerignore
================================================
**/.DS_Store
build/
htmlcov/
icloudpd.egg-info
cron_script.sh
.vscode
**/__pycache__
.pytest_cache/
.cache
*.pyc
.idea
.coverage
.coverage.*
.git
.venv
venv
create/
Dockerfile.test-py310
.dockerignore.test
check_py310_compat.py

================================================
FILE: .gitattributes
================================================
* text eol=lf
*.png binary


================================================
FILE: .github/ISSUE_TEMPLATE/bug-report.md
================================================
---
name: Bug Report
about: provide information about a problem
title: 
labels: bug
assignees: ''

---
## Overview
[TIP]:  # ( DO NOT include your login-information when pasting in program output! )
[NOTE]: # ( Give a BRIEF summary about your problem )


## Steps to Reproduce
[NOTE]: # ( Provide a simple set of steps to reproduce this bug. Version of the app, OS/System, and minimum command line to reproduce. )
1. 
2. 
3. 


## Expected Behavior
[NOTE]: # ( Tell us what you expected to happen )


## Actual Behavior
[NOTE]: # ( Tell us what actually happens )


## Context
[NOTE]: # ( Tell us why expected behvior is valuable )

[NOTE]: # ( Give us any additional information you may have. )


================================================
FILE: .github/ISSUE_TEMPLATE/feature-request.md
================================================
---
name: Generic Ticket
about: generic/empty ticket for discussion/questions that are not bugs or enhancements
title: 
labels: 
assignees: ''

---
## Summary
[NOTE]: # ( Provide a brief overview of what the new feature is all about )


## Context
[NOTE]: # ( Why does this feature matter to you? What unique circumstances do you have? )


================================================
FILE: .github/ISSUE_TEMPLATE/generic.md
================================================
---
name: Feature Request
about: tell us about a new feature or improvement you want
title: 
labels: enhancement
assignees: ''

---
## Summary
[NOTE]: # ( Provide a brief overview of what the new feature is all about )


## Context
[NOTE]: # ( Why does this feature matter to you? What unique circumstances do you have? )


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates

version: 2
updates:
  - package-ecosystem: "pip" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "monthly"
    open-pull-requests-limit: 2


================================================
FILE: .github/workflows/build-docs.yml
================================================
name: "Render docs"

on: workflow_call

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4

    - uses: docker/setup-buildx-action@v3
      id: setup

    - name: Cache Build
      uses: actions/cache@v4
      id: cache
      with:
        # if the list or anything in these folders expected to change, then cache needs to be cleared and rebuilt, because it is keyed only by pyproject.toml hash
        path: .cache
        key: build-doc-${{ hashFiles('pyproject.toml','Dockerfile.build-doc') }}
        restore-keys: |
          build-doc-
      
    - name: Create folders on cache miss
      run: |
        mkdir -p .cache/var/cache/apt
        mkdir -p .cache/var/lib/apt
        mkdir -p .cache/root/.cache/pip
        mkdir -p .cache/root/.distrib/rust
        mkdir -p .cache/root/.cargo/registry
      
    - name: Inject docker cache
      uses: reproducible-containers/buildkit-cache-dance@v3.1.2
      with:
        cache-map: |
          {
            ".cache/var/cache/apt": "/var/cache/apt",
            ".cache/var/lib/apt": "/var/lib/apt",
            ".cache/root/.cache/pip": "/root/.cache/pip",
            ".cache/root/.cargo/registry": "/root/.cargo/registry",
            ".cache/root/.distrib/rust": "/root/.distrib/rust"
          }

    - name: Build Doc
      uses: docker/build-push-action@v6
      with:
        push: false
        platforms: linux/arm64
        # do not cache layers
        file: Dockerfile.build-doc
        outputs: type=local,dest=docs/_build/html
        provenance: false
        context: .

    - name: Setup Pages
      id: pages
      uses: actions/configure-pages@v5

    - name: Upload artifact
      uses: actions/upload-pages-artifact@v3
      with:
        path: docs/_build/html/
         


================================================
FILE: .github/workflows/build-package.yml
================================================

name: Build and Package

on:
  workflow_call:
    inputs:
      icloudpd_version:
        required: true
        type: string

jobs:

  build_src:
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check -e . --group dev

    - name: Build Python Wheel
      run: |
        scripts/build

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-src
        if-no-files-found: error
        path: |
          dist/icloudpd*.whl

  get_version_thumbprint:
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install
      run: >
        export DEBIAN_FRONTEND=noninteractive && sudo apt-get update && sudo apt-get install -y tzdata locales-all &&
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check -e .

    - name: Get Version
      id: get_version
      run: >
        mkdir -p dist &&
        icloudpd --version | tee dist/icloudpd-version-thumbprint.txt
          
    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-version-thumbprint
        if-no-files-found: error
        path: |
          dist/icloudpd-version-thumbprint.txt
          
  get_expected_version_linux_apt:
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        export DEBIAN_FRONTEND=noninteractive && sudo apt-get update && sudo apt-get install -y tzdata locales-all &&
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check -e .

    - name: Get Version
      id: get_version
      run: |
        echo expected_version=$(TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version) >> $GITHUB_OUTPUT
          
    - name: Log version
      run: |
        echo "expected_version=${{steps.get_version.outputs.expected_version}}"

    outputs:
      expected_version: ${{steps.get_version.outputs.expected_version}}

  get_expected_version_linux_apk:
    runs-on: ubuntu-22.04
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Run version
      uses: addnab/docker-run-action@v3
      with:
        image: python:3.13-alpine3.19
        shell: sh
        options: -v ${{ github.workspace }}:/work 
        run: >
          export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
          apk add --update tzdata musl-locales musl-locales-lang &&
          cd /work &&
          python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
          pip3 install . &&
          TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result

    - name: Get Version
      id: get_version
      run: |
        echo expected_version=$(cat .result) >> $GITHUB_OUTPUT
          
    - name: Log version
      run: |
        echo "expected_version=${{steps.get_version.outputs.expected_version}}"

    outputs:
      expected_version: ${{steps.get_version.outputs.expected_version}}


  get_expected_version_macos:
    runs-on: macos-14
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: |
        pip3 install --disable-pip-version-check -e .

    - name: Get Version
      id: get_version
      run: |
        echo expected_version=$(TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version) >> $GITHUB_OUTPUT
          
    - name: Log version
      run: |
        echo "expected_version=${{steps.get_version.outputs.expected_version}}"

    outputs:
      expected_version: ${{steps.get_version.outputs.expected_version}}
  
  build_linux_apt:
    # 24.02 gives futex error during apt: https://github.com/actions/runner-images/issues/9977
    runs-on: ${{ (matrix.platform[1] == 'arm64' || matrix.platform[1] == 'arm32v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    name: Build Linux ${{ matrix.platform[1] }} Debian
    strategy:
      fail-fast: false
      matrix:
        platform:
          - [
            "linux/amd64",
            "amd64",
          ]
          - [
            "linux/arm64",
            "arm64",
          ]
          - [
            "linux/arm/v7",
            "arm32v7",
          ]
    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    # ARM32v7 runs natively on ARM64 runners - no QEMU needed

    - uses: docker/setup-buildx-action@v3
      id: setup

    - name: Cache Build
      uses: actions/cache@v4
      id: cache
      with:
        # if the list or anything in these folders expected to change, then cache needs to be cleared and rebuilt, because it is keyed only by pyproject.toml hash
        path: .cache
        key: build-${{ matrix.platform[1] }}-${{ hashFiles('pyproject.toml','Dockerfile.build') }}
        restore-keys: |
          build-${{ matrix.platform[1] }}-
      
    - name: Create folders on cache miss
      run: |
        mkdir -p .cache/var/cache/apt
        mkdir -p .cache/var/lib/apt
        mkdir -p .cache/root/.cache/pip
        mkdir -p .cache/root/.distrib/rust
        mkdir -p .cache/root/.cargo/registry
      
    - name: Inject docker cache
      uses: reproducible-containers/buildkit-cache-dance@v3.1.2
      with:
        cache-map: |
          {
            ".cache/var/cache/apt": "/var/cache/apt",
            ".cache/var/lib/apt": "/var/lib/apt",
            ".cache/root/.cache/pip": "/root/.cache/pip",
            ".cache/root/.cargo/registry": "/root/.cargo/registry",
            ".cache/root/.distrib/rust": "/root/.distrib/rust"
          }

    - name: Build
      uses: docker/build-push-action@v6
      with:
        push: false
        platforms: ${{ matrix.platform[0] }}
        # do not cache layers
        file: Dockerfile.build
        outputs: type=local,dest=dist
        provenance: false
        context: .

    - name: Reorg contents
      continue-on-error: true
      run: |
        mv dist/icloud dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.platform[1] }}
        mv dist/icloudpd dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.platform[1] }}
        
    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-bin-linux-${{ matrix.platform[1] }}-apt
        if-no-files-found: error
        path: |
          dist/icloud*
  
  clone_src_whl:
    runs-on: ubuntu-22.04
    needs:
      - build_src
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download src
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-src
        path: |
          dist

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check --group dev

    - name: Build Dummy Python Wheel
      run: |
        scripts/clone_whl_version ${{inputs.icloudpd_version}} 0.0.1234567890

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-dummywhl-src
        if-no-files-found: error
        path: |
          dist/icloudpd-0.0.1234567890-*.whl

  clone_linux_whl:
    runs-on: ubuntu-22.04
    needs:
      - build_linux_apt
      - build_linux_apk
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download bin
      uses: actions/download-artifact@v4
      with:
        pattern: icloudpd-bin-linux-*
        merge-multiple: true
        path: |
          dist

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check --group dev

    - name: Build Dummy Python Wheel
      run: |
        scripts/clone_whl_version ${{inputs.icloudpd_version}} 0.0.1234567890

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-dummywhl-linux
        if-no-files-found: error
        path: |
          dist/icloudpd-0.0.1234567890-*.whl
        
  clone_macos_whl:
    runs-on: ubuntu-22.04
    needs:
      - build_macos
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download bin
      uses: actions/download-artifact@v4
      with:
        pattern: icloudpd-bin-macos-*
        merge-multiple: true
        path: |
          dist

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check --group dev

    - name: Build Dummy Python Wheel
      run: |
        scripts/clone_whl_version ${{inputs.icloudpd_version}} 0.0.1234567890

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-dummywhl-macos
        if-no-files-found: error
        path: |
          dist/icloudpd-0.0.1234567890-*.whl
  
  clone_windows_whl:
    runs-on: ubuntu-22.04
    needs:
      - build_windows
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download bin
      uses: actions/download-artifact@v4
      with:
        pattern: icloudpd-bin-windows-*
        merge-multiple: true
        path: |
          dist

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check --group dev

    - name: Build Dummy Python Wheel
      run: |
        scripts/clone_whl_version ${{inputs.icloudpd_version}} 0.0.1234567890

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-dummywhl-windows
        if-no-files-found: error
        path: |
          dist/icloudpd-0.0.1234567890-*.whl
        
  build_linux_apk:
    # 24.02 gives futex error during apt: https://github.com/actions/runner-images/issues/9977
    runs-on: ${{ (matrix.platform[1] == 'arm64' || matrix.platform[1] == 'arm32v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    name: Build Linux ${{ matrix.platform[1] }} Alpine
    strategy:
      fail-fast: false
      matrix:
        platform:
          - [
            "linux/amd64",
            "amd64",
          ]
          - [
            "linux/arm64",
            "arm64",
          ]
          - [
            "linux/arm/v7",
            "arm32v7",
          ]
    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    # ARM32v7 runs natively on ARM64 runners - no QEMU needed

    - uses: docker/setup-buildx-action@v3
      id: setup
    
    - name: Cache Folders Build
      uses: actions/cache@v4
      id: cache
      with:
        # if the list or anything in these folders expected to change, then cache needs to be cleared and rebuilt, because it is keyed only by pyproject.toml hash
        path: .cache
        key: build-musl-${{ matrix.platform[1] }}-${{ hashFiles('pyproject.toml','Dockerfile.build-musl') }}
        restore-keys: |
          build-musl-${{ matrix.platform[1] }}-

    - name: Create folders on cache miss
      run: |
        mkdir -p .cache/var/cache/apk
        mkdir -p .cache/root/.cache/pip
      
    - name: Inject docker cache
      uses: reproducible-containers/buildkit-cache-dance@v3.1.2
      with:
        cache-map: |
          {
            ".cache/var/cache/apk": "/var/cache/apk",
            ".cache/root/.cache/pip": "/root/.cache/pip"
          }
              
    - name: Build
      uses: docker/build-push-action@v6
      with:
        push: false
        platforms: ${{ matrix.platform[0] }}
        # do not cache layers
        file: Dockerfile.build-musl
        outputs: type=local,dest=dist
        provenance: false
        context: .

    - name: Reorg contents
      continue-on-error: true
      run: |
        mv dist/icloud dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.platform[1] }}
        mv dist/icloudpd dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.platform[1] }}
        
    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-bin-linux-${{ matrix.platform[1] }}-apk
        if-no-files-found: error
        path: |
          dist/icloud*
  
  build_macos:
    # earliest possible mac for better compatibility
    runs-on: macos-13
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check -e . --group dev

    - name: Build Release Executables
      run: |
        scripts/build_bin1 icloudpd && \
        scripts/build_bin1 icloud && \
        mv dist/icloud dist/icloud-${{inputs.icloudpd_version}}-macos-amd64 && \
        mv dist/icloudpd dist/icloudpd-${{inputs.icloudpd_version}}-macos-amd64

    - name: Build Python Binary Wheel
      run: |
        scripts/build_binary_dist_macos ${{inputs.icloudpd_version}}

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-bin-macos-amd64
        if-no-files-found: error
        path: |
          dist/icloud*

  build_windows:
    # earliest possible win for better compatibility
    runs-on: windows-2022
    defaults:
      run:
        shell: bash

    steps:
    - uses: actions/checkout@v4

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python 3.13
      uses: actions/setup-python@v5
      with:
        python-version: '3.13'
    
    - name: Install Dev dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check -e . --group dev

    - name: Build Release Executables
      run: |
        scripts/build_bin1 icloudpd && \
        scripts/build_bin1 icloud && \
        mv dist/icloud dist/icloud-${{inputs.icloudpd_version}}-windows-amd64 && \
        mv dist/icloudpd dist/icloudpd-${{inputs.icloudpd_version}}-windows-amd64

    - name: Build Python Binary Wheel
      run: |
        scripts/build_binary_dist_windows ${{inputs.icloudpd_version}}

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-bin-windows-amd64
        if-no-files-found: error
        path: |
          dist/icloud*

  build_docker:
    runs-on: ubuntu-22.04
    needs: 
      # - build_linux_apt
      - build_linux_apk # docker is musl only

    steps:
    - uses: actions/checkout@v4

    - name: Download artifacts
      uses: actions/download-artifact@v4
      with: 
        pattern: icloudpd-bin-linux-*
        merge-multiple: true
        path: | 
          dist 
 
    - name: Set up QEMU
      uses: docker/setup-qemu-action@v3

    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v3
      with:
        version: v0.12.0

    - name: Build Release Docker
      uses: docker/build-push-action@v5
      with:
        context: .
        file: ./Dockerfile
        platforms: linux/amd64,linux/arm64,linux/arm/v7 #,linux/arm/v5
        push: false
        outputs: type=oci,dest=dist/icloudpd-${{inputs.icloudpd_version}}-oci.tar
        load: false  # load into docker 
        # tags: | 
        #   icloudpd/icloudpd:latest 
        #   icloudpd/icloudpd:${{inputs.icloudpd_version}} 
        #   icloudpd/icloudpd:commit-${{ github.sha }} 
 
    - name: Upload artifacts 
      uses: actions/upload-artifact@v4 
      with: 
        name: icloudpd-oci 
        if-no-files-found: error 
        path: | 
          dist/icloud*.tar

  build_npm:
    runs-on: ubuntu-22.04
    needs: 
      - build_linux_apt
      # - build_linux_apk npm is glibc only
      - build_macos
      - build_windows
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-bin-*
          merge-multiple: true
          path: |
            dist

      - name: Build Package
        run: |
          scripts/build_npm ${{inputs.icloudpd_version}}

      - name: Upload artifacts 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-npm
          if-no-files-found: error 
          path: | 
            dist/npm/

  compatibility_linux_pip_apt:
    name: "Compat PIP ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apt"
    strategy: 
      fail-fast: false
      matrix:
        image: 
          - [ 
            "python3_13", #variant name
            "python:3.13", #image
            ] 
          # apt-get does not work for debian 6 anymore
          # - [ 
          #   "debian_6_squeeze", #variant name
          #   "debian:squeeze", #image
          #   ] 
          # apt-get does not work for debian 7 anymore
          # - [ 
          #   "debian_7_wheezy", #variant name
          #   "debian:wheezy", #image
          #   ] 
          # apt-get does not work for debian 8 anymore
          # - [ 
          #   "debian_8_jessie", #variant name
          #   "debian:jessie", #image
          #   ] 
          # apt-get does not work for debian 7 anymore
          # - [ 
          #   "debian_9_stretch", #variant name
          #   "debian:stretch", #image
          #   ] 
          - [ 
            "debian_10_buster", #variant name
            "debian:buster", #image
            ] 
          - [ 
            "debian_11_bullseye", #variant name
            "debian:bullseye", #image
            ] 
          - [ 
            "debian_12_bookworm", #variant name
            "debian:bookworm", #image
            ] 
          # apt-get does not work for ubuntu 12 anymore
          # - [ 
          #   "ubuntu_12_precise", #variant name
          #   "ubuntu:precise", #image
          #   ] 
          # apt-get does not work have venv for ubuntu 14 -- TBD
          # - [ 
          #   "ubuntu_14_trusty", #variant name
          #   "ubuntu:trusty", #image
          #   ] 
          - [ 
            "ubuntu_16_xenial", #variant name
            "ubuntu:xenial", #image
            ] 
          - [ 
            "ubuntu_18_bionic", #variant name
            "ubuntu:bionic", #image
            ] 
          - [ 
            "ubuntu_20_focal", #variant name
            "ubuntu:focal", #image
            ] 
          - [ 
            "ubuntu_22_jammy", #variant name
            "ubuntu:jammy", #image
            ] 
          - [ 
            "ubuntu_24_noble", #variant name
            "ubuntu:noble", #image
            ] 
        prop: 
          - [
            "amd64",  # variant name
            "amd64",  # platform spec
            "",  # image prefix
          ]
          - [
            "arm64",  # variant name
            "arm64",  # platform spec
            "",  # image prefix
          ]
          - [
            "arm32v7",  # variant name
            "arm/v7",  # platform spec
            "arm32v7/",  # image prefix
          ]
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    needs:
      - clone_linux_whl
      - clone_src_whl
    defaults:
      run:
        shell: bash

    steps:

      - name: Get image
        id: get_image 
        run: | 
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT
 
      - name: Make folder for compatibility
        if: steps.get_image.outputs.digest != '' 
        run: |
          mkdir compatibility

      - name: Make folder for tzlc
        if: steps.get_image.outputs.digest != '' 
        run: |
          mkdir tzlc
  
      - name: Download artifacts (src)
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-src
          path: |
            dist

      - name: Download artifacts (whl)
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-linux
          path: |
            dist

      # fails with "icloud: Failed to stat /proc/self/exe: Bad file descriptor" in bookwork arm64
      # - name: Set up QEMU
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed

      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: steps.get_image.outputs.digest != '' 
        id: run_test
        uses: addnab/docker-run-action@v3
        continue-on-error: true
        with:
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }}
          shell: bash
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }}
          run: >
            echo "install default python..." &&
            export DEBIAN_FRONTEND=noninteractive && apt-get update -y && apt-get install -y python3 python3-venv &&
            echo "create venv and active..." &&
            cd /work &&
            python3 -m venv .venv &&
            . .venv/bin/activate &&
            echo "install icloudpd..." &&
            pip3 install --disable-pip-version-check --find-links /work/dist icloudpd==0.0.1234567890 &&
            echo "test icloud..." &&
            icloud --version &&
            echo "test icloudpd..." &&
            icloudpd --version &&
            echo "check if src..." &&
            (pip3 list --disable-pip-version-check | grep keyring | head -n 1 > /work/compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass)

          # touch /work/compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass

      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_test.outcome == 'failure' }} 
        run: |
          touch compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail

      - name: Upload compatibility result
        if: steps.get_image.outputs.digest != '' 
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-compatibility-linux-pip-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error
          path: |
            compatibility/*

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: steps.get_image.outputs.digest != '' 
        id: run_tzlc
        uses: addnab/docker-run-action@v3
        continue-on-error: true
        with:
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }}
          shell: bash
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }}
          run: >
            echo "install default python, tzdata, locales..." &&
            export DEBIAN_FRONTEND=noninteractive && apt-get update -y && apt-get install -y python3 python3-venv tzdata locales-all &&
            echo "create venv and active..." &&
            cd /work &&
            python3 -m venv .venv &&
            . .venv/bin/activate &&
            echo "install icloudpd..." &&
            pip3 install --disable-pip-version-check --find-links /work/dist icloudpd==0.0.1234567890 &&
            echo "test icloud..." &&
            icloud --version &&
            echo "test icloudpd..." &&
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result &&
            cat .result &&
            mv .result /work/tzlc/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass
    
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_tzlc.outcome == 'failure' }} 
        run: |
          touch tzlc/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail
    
      - name: Upload tzlc result
        if: steps.get_image.outputs.digest != '' 
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-tzlc-linux-pip-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error
          path: |
            tzlc/*
    
  compatibility_linux_pip_apk:
    name: "Compat PIP ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk"
    strategy: 
      fail-fast: false
      matrix:
        image: 
          - [ 
            "python3_13_alpine3_19", #variant name
            "python:3.13-alpine3.19", #image
            ] 
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
          # - [ 
          #   "alpine_3_12", #variant name
          #   "alpine:3.12", #image
          #   ] 
          # - [ 
          #   "alpine_3_11", #variant name
          #   "alpine:3.11", #image
          #   ] 
          # - [ 
          #   "alpine_3_10", #variant name
          #   "alpine:3.10", #image
          #   ] 
        prop: 
          - [
            "amd64",  # variant name
            "amd64",  # platform spec
            "",  # image prefix
          ]
          - [
            "arm64",  # variant name
            "arm64",  # platform spec
            "",  # image prefix
          ]
          - [
            "arm32v7",  # variant name
            "arm/v7",  # platform spec
            "arm32v7/",  # image prefix
          ]
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    needs: 
      - clone_linux_whl
      - clone_src_whl
    defaults:
      run:
        shell: bash

    steps:

      - name: Get image
        id: get_image 
        run: | 
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT

      - name: Make folder for compatibility
        if: steps.get_image.outputs.digest != '' 
        run: |
          mkdir compatibility

      - name: Make folder for tzlc
        if: steps.get_image.outputs.digest != '' 
        run: |
          mkdir tzlc
  
      - name: Download artifacts (src)
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-src
          path: |
            dist

      - name: Download artifacts (linux)
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-linux
          path: |
            dist

      # - name: Set up QEMU
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed

      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: steps.get_image.outputs.digest != '' 
        id: run_test
        uses: addnab/docker-run-action@v3
        continue-on-error: true
        with:
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }}
          shell: sh
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }}
          run: >
            echo "install default python..." &&
            apk update && apk add python3 &&
            echo "create venv and active..." &&
            cd /work &&
            python3 -m venv .venv &&
            . .venv/bin/activate &&
            echo "install icloudpd..." &&
            pip3 install --disable-pip-version-check --find-links /work/dist icloudpd==0.0.1234567890 &&
            echo "test icloud..." &&
            icloud --version &&
            echo "test icloudpd..." &&
            icloudpd --version &&
            echo "check if src..." &&
            (pip3 list --disable-pip-version-check | grep keyring | head -n 1 > /work/compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass)
          # touch /work/compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass

      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_test.outcome == 'failure' }} 
        run: |
          touch compatibility/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail

      - name: Upload compatibility result
        if: steps.get_image.outputs.digest != '' 
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-compatibility-linux-pip-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error
          path: |
            compatibility/*

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: steps.get_image.outputs.digest != '' 
        id: run_tzlc
        uses: addnab/docker-run-action@v3
        continue-on-error: true
        with:
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }}
          shell: sh
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }}
          run: >
            echo "install default python, tz, locale..." &&
            export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
            apk update && apk add python3 tzdata musl-locales musl-locales-lang &&
            echo "create venv and active..." &&
            cd /work &&
            python3 -m venv .venv &&
            . .venv/bin/activate &&
            echo "install icloudpd..." &&
            pip3 install --disable-pip-version-check --find-links /work/dist icloudpd==0.0.1234567890 &&
            echo "test icloudpd..." &&
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result &&
            cat .result &&
            mv .result /work/tzlc/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass

      - name: Record tzlc failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_tzlc.outcome == 'failure' }} 
        run: |
          touch tzlc/pip.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail

      - name: Upload tzlc result
        if: steps.get_image.outputs.digest != '' 
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-tzlc-linux-pip-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error
          path: |
            tzlc/*

  compatibility_macos_pip:
    name: "Compat PIP ${{ matrix.prop[0] }} ${{ matrix.prop[1] }}"
    strategy: 
      fail-fast: false
      matrix: 
        prop: 
          - [
            "macos-13",  # os
            "amd64",  # platform spec
          ]
          - [
            "macos-14",  # os
            "arm64",  # platform spec
          ]
    runs-on: ${{ matrix.prop[0] }}
    needs: 
      - clone_macos_whl
      - clone_src_whl

    steps:

      - name: Make folder for compatibility
        run: |
          mkdir compatibility

      - name: Make folder for tzlc
        run: |
          mkdir tzlc
  
      - name: Download artifacts (src)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-src
          path: |
            dist

      - name: Download artifacts (macos)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-macos
          path: |
            dist

      - name: Run test for ${{ matrix.prop[0] }} on ${{ matrix.prop[1] }}
        id: run_test
        continue-on-error: true
        run: >
          echo "create venv and active..." &&
          python3 -m venv .venv &&
          . .venv/bin/activate &&
          pip3 install --disable-pip-version-check --find-links dist icloudpd==0.0.1234567890 &&
          icloud --version &&
          icloudpd --version &&
          touch compatibility/pip.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass

        # &&
        # echo "check if src..."  &&
        # pip3 list --disable-pip-version-check | grep keyring | head -n 1 > compatibility/pip.${{ matrix.os }}.amd64.pass

      - name: Record failure for ${{ matrix.prop[0] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_test.outcome == 'failure' }} 
        run: |
          touch compatibility/pip.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail

      - name: Upload compatibility result
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-compatibility-macos-pip-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error
          path: |
            compatibility/*

      - name: Run tzlc for ${{ matrix.prop[0] }} on ${{ matrix.prop[1] }}
        id: run_tzlc
        continue-on-error: true
        run: >
          echo "create venv and active..." &&
          python3 -m venv .venv &&
          . .venv/bin/activate &&
          pip3 install --disable-pip-version-check --find-links dist icloudpd==0.0.1234567890 &&
          TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result &&
          cat .result &&
          mv .result tzlc/pip.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass

        # &&
        # echo "check if src..."  &&
        # pip3 list --disable-pip-version-check | grep keyring | head -n 1 > compatibility/pip.${{ matrix.os }}.amd64.pass
    
      - name: Record failure tzlc for ${{ matrix.prop[0] }} on ${{ matrix.prop[1] }}
        if: ${{ steps.run_tzlc.outcome == 'failure' }} 
        run: |
          touch tzlc/pip.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail

      - name: Upload tzlc result
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-tzlc-macos-pip-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error
          path: |
            tzlc/*
      
  compatibility_windows_pip:
    name: "Compat PIP ${{ matrix.os }} on amd64"
    strategy: 
      fail-fast: false
      matrix: 
        os: [ "windows-2025", "windows-2022" ]
    runs-on: ${{ matrix.os }}
    needs: 
      - clone_windows_whl
      - clone_src_whl

    steps:

      - name: Make folder for compatibility
        run: |
          mkdir compatibility

      - name: Download artifacts (src)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-src
          path: |
            dist

      - name: Download artifacts (windows)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-dummywhl-windows
          path: |
            dist

      # https://learn.microsoft.com/en-us/cpp/c-runtime-library/language-strings?view=msvc-170&redirectedfrom=MSDN
      # languages
      - name: Run test for ${{ matrix.os }} on amd64
        id: run_test
        continue-on-error: true
        run: >
          pip3 install --disable-pip-version-check --find-links dist icloudpd==0.0.1234567890 &&
          icloud --version &&
          icloudpd --version &&
          touch compatibility/pip.${{ matrix.os }}.amd64.pass

        # &&
        # echo "check if src..."  &&
        # pip3 list --disable-pip-version-check | grep keyring | head -n 1 > compatibility/pip.${{ matrix.os }}.amd64.pass

      - name: Record failure for ${{ matrix.os }} on amd64
        if: ${{ steps.run_test.outcome == 'failure' }} 
        run: |
          touch compatibility/pip.${{ matrix.os }}.amd64.fail

      - name: Upload compatibility result
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-compatibility-windows-pip-${{ matrix.os }}-amd64
          if-no-files-found: error
          path: |
            compatibility/*

  compatibility_linux_bin_apt: 
    name: "Compat BIN ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apt" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [  
            "debian_6_squeeze", #variant name 
            "debian:squeeze", #image 
            ]  
          - [  
            "debian_7_wheezy", #variant name 
            "debian:wheezy", #image 
            ]  
          - [  
            "debian_8_jessie", #variant name 
            "debian:jessie", #image 
            ]  
          - [  
            "debian_9_stretch", #variant name 
            "debian:stretch", #image 
            ]  
          - [  
            "debian_10_buster", #variant name 
            "debian:buster", #image 
            ]  
          - [  
            "debian_11_bullseye", #variant name 
            "debian:bullseye", #image 
            ]  
          - [  
            "debian_12_bookworm", #variant name 
            "debian:bookworm", #image 
            ]  
          - [  
            "ubuntu_12_precise", #variant name 
            "ubuntu:precise", #image 
            ]  
          - [  
            "ubuntu_14_trusty", #variant name 
            "ubuntu:trusty", #image 
            ]  
          - [  
            "ubuntu_16_xenial", #variant name 
            "ubuntu:xenial", #image 
            ]  
          - [  
            "ubuntu_18_bionic", #variant name 
            "ubuntu:bionic", #image 
            ]  
          - [  
            "ubuntu_20_focal", #variant name 
            "ubuntu:focal", #image 
            ]  
          - [  
            "ubuntu_22_jammy", #variant name 
            "ubuntu:jammy", #image 
            ]  
          - [  
            "ubuntu_24_noble", #variant name 
            "ubuntu:noble", #image 
            ]  
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} 
    needs: 
      - build_linux_apt
      # - build_linux_apk
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 
  
      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir tzlc 
   
      - name: Download artifacts
        if: steps.get_image.outputs.digest != ''  
        uses: actions/download-artifact@v4 
        with: 
          pattern: icloudpd-bin-linux-*
          merge-multiple: true
          path: | 
            dist 
 
      # fails with "icloud: Failed to stat /proc/self/exe: Bad file descriptor" in bookwork arm64 
      # - name: Set up QEMU 
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3 
 
      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
 
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            echo "test icloud..." && 
            dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --version && 
            echo "test icloudpd..." && 
            dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --version && 
            touch /work/compatibility/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
 
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-bin-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install tzdata and locales..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y tzdata locales-all &&
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            echo "test icloudpd..." && 
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --use-os-locale --version 1>.result && 
            cat .result &&
            mv .result /work/tzlc/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-bin-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            tzlc/* 

  compatibility_linux_bin_musl_apt: 
    name: "Compat BIN-M ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apt" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [  
            "debian_6_squeeze", #variant name 
            "debian:squeeze", #image 
            ]  
          - [  
            "debian_7_wheezy", #variant name 
            "debian:wheezy", #image 
            ]  
          - [  
            "debian_8_jessie", #variant name 
            "debian:jessie", #image 
            ]  
          - [  
            "debian_9_stretch", #variant name 
            "debian:stretch", #image 
            ]  
          - [  
            "debian_10_buster", #variant name 
            "debian:buster", #image 
            ]  
          - [  
            "debian_11_bullseye", #variant name 
            "debian:bullseye", #image 
            ]  
          - [  
            "debian_12_bookworm", #variant name 
            "debian:bookworm", #image 
            ]  
          - [  
            "ubuntu_12_precise", #variant name 
            "ubuntu:precise", #image 
            ]  
          - [  
            "ubuntu_14_trusty", #variant name 
            "ubuntu:trusty", #image 
            ]  
          - [  
            "ubuntu_16_xenial", #variant name 
            "ubuntu:xenial", #image 
            ]  
          - [  
            "ubuntu_18_bionic", #variant name 
            "ubuntu:bionic", #image 
            ]  
          - [  
            "ubuntu_20_focal", #variant name 
            "ubuntu:focal", #image 
            ]  
          - [  
            "ubuntu_22_jammy", #variant name 
            "ubuntu:jammy", #image 
            ]  
          - [  
            "ubuntu_24_noble", #variant name 
            "ubuntu:noble", #image 
            ]  
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} 
    needs: 
      # - build_linux_apt
      - build_linux_apk
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 
  
      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir tzlc 
   
      - name: Download artifacts
        if: steps.get_image.outputs.digest != ''  
        uses: actions/download-artifact@v4 
        with: 
          pattern: icloudpd-bin-linux-*
          merge-multiple: true
          path: | 
            dist 
 
      # fails with "icloud: Failed to stat /proc/self/exe: Bad file descriptor" in bookwork arm64 
      # - name: Set up QEMU 
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3 
 
      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
 
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            echo "test icloud..." && 
            dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --version && 
            echo "test icloudpd..." && 
            dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --version && 
            touch /work/compatibility/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
 
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-bin-musl-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install tzdata and locales..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y tzdata locales-all &&
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            echo "test icloudpd..." && 
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --use-os-locale --version 1>.result && 
            cat .result &&
            mv .result /work/tzlc/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-bin-musl-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            tzlc/* 


  compatibility_linux_bin_apk: 
    name: "Compat BIN ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
          - [ 
            "alpine_3_12", #variant name
            "alpine:3.12", #image
            ] 
          - [ 
            "alpine_3_11", #variant name
            "alpine:3.11", #image
            ] 
          - [ 
            "alpine_3_10", #variant name
            "alpine:3.10", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    needs: 
      - build_linux_apt
      # - build_linux_apk
    defaults: 
      run: 
        shell: bash 
  
    steps: 
  
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 
  
      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir tzlc 
    
      - name: Download artifacts
        if: steps.get_image.outputs.digest != ''  
        uses: actions/download-artifact@v4 
        with: 
          pattern: icloudpd-bin-linux-*
          merge-multiple: true
          path: | 
            dist 
  
      # fails with "icloud: Failed to stat /proc/self/exe: Bad file descriptor" in bookwork arm64 
      # - name: Set up QEMU 
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3 
  
      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
  
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            echo "test icloud..." && 
            dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --version && 
            echo "test icloudpd..." && 
            dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --version && 
            touch /work/compatibility/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-bin-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install tzdata and locales ..." && 
            export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
            apk update && apk add tzdata musl-locales musl-locales-lang &&
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} && 
            echo "test icloudpd..." && 
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 dist/icloudpd-${{inputs.icloudpd_version}}-linux-${{ matrix.prop[0] }} --use-os-locale --version 1>.result && 
            cat .result &&
            mv .result /work/tzlc/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/bin.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-bin-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            tzlc/* 
          

  compatibility_linux_bin_musl_apk: 
    name: "Compat BIN-M ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
          - [ 
            "alpine_3_12", #variant name
            "alpine:3.12", #image
            ] 
          - [ 
            "alpine_3_11", #variant name
            "alpine:3.11", #image
            ] 
          - [ 
            "alpine_3_10", #variant name
            "alpine:3.10", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    needs: 
      # - build_linux_apt
      - build_linux_apk
    defaults: 
      run: 
        shell: bash 
  
    steps: 
  
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 
  
      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != ''  
        run: | 
          mkdir tzlc 
    
      - name: Download artifacts
        if: steps.get_image.outputs.digest != ''  
        uses: actions/download-artifact@v4 
        with: 
          pattern: icloudpd-bin-linux-*
          merge-multiple: true
          path: | 
            dist 
  
      # fails with "icloud: Failed to stat /proc/self/exe: Bad file descriptor" in bookwork arm64 
      # - name: Set up QEMU 
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3 
  
      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
  
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            echo "test icloud..." && 
            dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --version && 
            echo "test icloudpd..." && 
            dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --version && 
            touch /work/compatibility/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-bin-musl-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install tzdata and locales ..." && 
            export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
            apk update && apk add tzdata musl-locales musl-locales-lang &&
            echo "prep test..." && 
            cd /work && 
            chmod +x dist/icloud-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} && 
            echo "test icloudpd..." && 
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 dist/icloudpd-${{inputs.icloudpd_version}}-linux-musl-${{ matrix.prop[0] }} --use-os-locale --version 1>.result && 
            cat .result &&
            mv .result /work/tzlc/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/bin-musl.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-bin-musl-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            tzlc/* 

  compatibility_macos_bin: 
    name: "Compat BIN ${{ matrix.prop[0] }} ${{ matrix.prop[1] }}" 
    strategy:  
      fail-fast: false 
      matrix:  
        prop:  
          - [ 
            "macos-13", 
            "amd64", 
            ] 
          - [ 
            "macos-14", 
            "arm64", 
            ] 
 
    runs-on: ${{ matrix.prop[0] }} 
    needs: [ build_macos ] 
 
    steps: 
 
      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        run: | 
          mkdir tzlc 
   
      - name: Download artifacts 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-bin-macos-amd64
          path: | 
            dist 
 
      - name: Run test for ${{ matrix.prop[0] }} on amd64 
        id: run_test 
        continue-on-error: true 
        run: > 
          echo "prep test..." && 
          chmod +x dist/icloud-${{inputs.icloudpd_version}}-macos-amd64 && 
          chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-macos-amd64 && 
          echo "test icloud..." && 
          dist/icloud-${{inputs.icloudpd_version}}-macos-amd64 --version && 
          echo "test icloudpd..." && 
          dist/icloudpd-${{inputs.icloudpd_version}}-macos-amd64 --version && 
          touch compatibility/bin.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
 
      - name: Record failure for ${{ matrix.prop[0] }} on amd64 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-macos-bin-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[0] }} on amd64 
        id: run_tzlc 
        continue-on-error: true 
        run: > 
          echo "prep test..." && 
          chmod +x dist/icloud-${{inputs.icloudpd_version}}-macos-amd64 && 
          chmod +x dist/icloudpd-${{inputs.icloudpd_version}}-macos-amd64 && 
          echo "test icloudpd..." && 
          TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 dist/icloudpd-${{inputs.icloudpd_version}}-macos-amd64 --use-os-locale --version 1>.result && 
          cat .result &&
          mv .result tzlc/bin.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[0] }} on amd64 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/bin.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
  
      - name: Upload tzlc result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-macos-bin-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            tzlc/* 
                
  compatibility_windows_bin: 
    name: "Compat BIN ${{ matrix.prop[0] }} on amd64" 
    strategy:  
      fail-fast: false 
      matrix:  
        prop:  
          - [ 
            "windows-2025", # GH os 
            "windows", # file suffix 
            ] 
          - [ 
            "windows-2022", 
            "windows", 
            ] 
 
    runs-on: ${{ matrix.prop[0] }} 
    needs: [ build_windows ] 
 
    steps: 
 
      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 
 
      - name: Download artifacts 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-bin-windows-amd64 
          path: | 
            dist 
 
      - name: Run test for ${{ matrix.prop[0] }} on amd64 
        id: run_test 
        continue-on-error: true 
        run: > 
          echo "test icloud..." && 
          dist/icloud-${{inputs.icloudpd_version}}-${{ matrix.prop[1] }}-amd64.exe --version && 
          echo "test icloudpd..." && 
          dist/icloudpd-${{inputs.icloudpd_version}}-${{ matrix.prop[1] }}-amd64.exe --version && 
 
          touch compatibility/bin.${{ matrix.prop[0] }}.amd64.pass 
 
      - name: Record failure for ${{ matrix.prop[0] }} on amd64 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/bin.${{ matrix.prop[0] }}.amd64.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-windows-bin-${{ matrix.prop[0] }}-amd64
          if-no-files-found: error 
          path: | 
            compatibility/* 

  compatibility_docker: 
    name: "Compat DOCKER ${{ matrix.image[1] }} ${{ matrix.prop[1] }}" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [  
            "linux", #variant name 
            "", #image 
            ]  
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }}
    needs: [ build_docker ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Install JQ
        run: sudo apt-get update && sudo apt-get install -y jq

      - name: Setup config 
        shell: bash 
        run: >
          sudo mkdir -p /etc/docker &&
          sudo touch /etc/docker/daemon.json &&
          (cat /etc/docker/daemon.json | jq -ne --arg registry 172.17.0.1:5000 'input? // {} | ."insecure-registries" = (."insecure-registries" // []) + [$registry]' | sudo tee /etc/docker/daemon.json)

      - name: Reload Docker 
        id: reload_docker
        continue-on-error: true 
        shell: bash 
        run: >
           sudo systemctl daemon-reload &&
           sudo systemctl restart docker

      - name: Journal for Docker
        if: steps.reload_docker.outcome == 'failure'
        shell: bash
        run: journalctl -xeu docker.service

      - name: Start Registry 
        if: steps.reload_docker.outcome != 'failure'
        shell: bash 
        run: docker run -d -p 5000:5000 registry:2 
 
      - name: Download artifacts 
        if: steps.reload_docker.outcome != 'failure'
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-oci
          path: | 
            dist 
 
      - name: Make folder for compatibility 
        if: steps.reload_docker.outcome != 'failure'
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.reload_docker.outcome != 'failure'
        run: | 
          mkdir tzlc 
            
      - name: Run skopeo 
        if: steps.reload_docker.outcome != 'failure'
        uses: addnab/docker-run-action@v3 
        with: 
          image: quay.io/skopeo/stable:v1.14.0 
          shell: sh 
          options: -v ${{ github.workspace }}/dist:/dist 
          run: > 
            (skopeo copy --preserve-digests --dest-tls-verify=false --all oci-archive:/dist/icloudpd-${{inputs.icloudpd_version}}-oci.tar docker://172.17.0.1:5000/icloudpd/icloudpd:commit-${{ github.sha }}) &&
            (skopeo copy --preserve-digests --src-tls-verify=false --dest-tls-verify=false --all docker://172.17.0.1:5000/icloudpd/icloudpd:commit-${{ github.sha }} docker://172.17.0.1:5000/icloudpd/icloudpd:latest)
  
      # - name: Set up QEMU 
      #   if: matrix.prop[1] != 'amd64'
      #   uses: docker/setup-qemu-action@v3 
 
      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
 
      - name: Run test on ${{ matrix.prop[1] }} 
        if: steps.reload_docker.outcome != 'failure'
        id: run_test 
        continue-on-error: true 
        run: >
          (docker run --rm -i --platform linux/${{ matrix.prop[1] }} 172.17.0.1:5000/icloudpd/icloudpd:commit-${{ github.sha }} icloudpd --version) &&
          (docker run --rm -i --platform linux/${{ matrix.prop[1] }} 172.17.0.1:5000/icloudpd/icloudpd:latest icloudpd --version)
 
      - name: Record success on ${{ matrix.prop[1] }} 
        if: steps.reload_docker.outcome != 'failure' && steps.run_test.outcome != 'failure'
        run: | 
          touch compatibility/docker.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
 
      - name: Record failure on ${{ matrix.prop[1] }} 
        if: steps.run_test.outcome == 'failure'
        run: | 
          touch compatibility/docker.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.reload_docker.outcome != 'failure'
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-docker-${{ matrix.image[0] }}-${{ matrix.prop[0] }}
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc on ${{ matrix.prop[1] }} 
        if: steps.reload_docker.outcome != 'failure'
        id: run_tzlc 
        continue-on-error: true 
        run: >
          (docker run --rm -i --platform linux/${{ matrix.prop[1] }} -e TZ=America/Los_Angeles -e LC_ALL=ru_RU.UTF-8 172.17.0.1:5000/icloudpd/icloudpd:latest icloudpd --use-os-locale --version) 1>.result
  
      - name: Record success tzlc on ${{ matrix.prop[1] }} 
        if: steps.reload_docker.outcome != 'failure' && steps.run_tzlc.outcome != 'failure'
        run: > 
          cat .result &&
          mv .result tzlc/docker.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc on ${{ matrix.prop[1] }} 
        if: steps.run_tzlc.outcome == 'failure'
        run: | 
          touch tzlc/docker.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.reload_docker.outcome != 'failure'
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-docker-${{ matrix.image[0] }}-${{ matrix.prop[0] }}
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
            
  compatibility_linux_npm_apt: 
    name: "Compat NPM ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apt" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          # apt-get does not work for debian 6 anymore
          # - [ 
          #   "debian_6_squeeze", #variant name
          #   "debian:squeeze", #image
          #   ] 
          # apt-get does not work for debian 7 anymore
          # - [ 
          #   "debian_7_wheezy", #variant name
          #   "debian:wheezy", #image
          #   ] 
          # apt-get does not work for debian 8 anymore
          # - [ 
          #   "debian_8_jessie", #variant name
          #   "debian:jessie", #image
          #   ] 
          # apt-get does not work for debian 9 anymore
          # - [ 
          #   "debian_9_stretch", #variant name
          #   "debian:stretch", #image
          #   ] 
          - [ 
            "debian_10_buster", #variant name
            "debian:buster", #image
            ] 
          - [ 
            "debian_11_bullseye", #variant name
            "debian:bullseye", #image
            ] 
          - [ 
            "debian_12_bookworm", #variant name
            "debian:bookworm", #image
            ] 
          - [ 
            "ubuntu_12_precise", #variant name
            "ubuntu:precise", #image
            ] 
          - [ 
            "ubuntu_14_trusty", #variant name
            "ubuntu:trusty", #image
            ] 
          - [ 
            "ubuntu_16_xenial", #variant name
            "ubuntu:xenial", #image
            ] 
          - [ 
            "ubuntu_18_bionic", #variant name
            "ubuntu:bionic", #image
            ] 
          - [ 
            "ubuntu_20_focal", #variant name
            "ubuntu:focal", #image
            ] 
          - [ 
            "ubuntu_22_jammy", #variant name
            "ubuntu:jammy", #image
            ] 
          - [ 
            "ubuntu_24_noble", #variant name
            "ubuntu:noble", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ${{ (matrix.prop[1] == 'arm64' || matrix.prop[1] == 'arm/v7') && 'ubuntu-22.04-arm' || 'ubuntu-22.04' }} 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 

      - name: Setup Node for Registry Server
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        if: steps.get_image.outputs.digest != '' 
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: debug, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - 0.0.0.0:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir tzlc 
   
      - name: Download artifacts
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        if: steps.get_image.outputs.digest != '' 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
 
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y npm &&
            echo "test icloudpd..." && 
            npm install -g --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} &&
            icloudpd --version  &&
            touch /work/compatibility/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
 
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm, tzdata, locales..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y npm tzdata locales-all&&
            echo "test icloudpd..." && 
            npm install -g --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} &&
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result &&
            cat .result &&
            mv .result /work/tzlc/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
  compatibility_linux_npx_apt: 
    name: "Compat NPX ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apt" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          # apt-get does not work for debian 6 anymore
          # - [ 
          #   "debian_6_squeeze", #variant name
          #   "debian:squeeze", #image
          #   ] 
          # apt-get does not work for debian 7 anymore
          # - [ 
          #   "debian_7_wheezy", #variant name
          #   "debian:wheezy", #image
          #   ] 
          # apt-get does not work for debian 8 anymore
          # - [ 
          #   "debian_8_jessie", #variant name
          #   "debian:jessie", #image
          #   ] 
          # apt-get does not work for debian 9 anymore
          # - [ 
          #   "debian_9_stretch", #variant name
          #   "debian:stretch", #image
          #   ] 
          - [ 
            "debian_10_buster", #variant name
            "debian:buster", #image
            ] 
          - [ 
            "debian_11_bullseye", #variant name
            "debian:bullseye", #image
            ] 
          - [ 
            "debian_12_bookworm", #variant name
            "debian:bookworm", #image
            ] 
          - [ 
            "ubuntu_12_precise", #variant name
            "ubuntu:precise", #image
            ] 
          - [ 
            "ubuntu_14_trusty", #variant name
            "ubuntu:trusty", #image
            ] 
          - [ 
            "ubuntu_16_xenial", #variant name
            "ubuntu:xenial", #image
            ] 
          - [ 
            "ubuntu_18_bionic", #variant name
            "ubuntu:bionic", #image
            ] 
          - [ 
            "ubuntu_20_focal", #variant name
            "ubuntu:focal", #image
            ] 
          - [ 
            "ubuntu_22_jammy", #variant name
            "ubuntu:jammy", #image
            ] 
          - [ 
            "ubuntu_24_noble", #variant name
            "ubuntu:noble", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ubuntu-22.04 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 

      - name: Checkout code
        if: steps.get_image.outputs.digest != '' 
        uses: actions/checkout@v4

      - name: Setup Node for Registry Server
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        if: steps.get_image.outputs.digest != '' 
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: debug, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - 0.0.0.0:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir tzlc 
   
      - name: Download artifacts
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        if: steps.get_image.outputs.digest != '' 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
 
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: bash 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y npm &&
            /work/scripts/npx_optional_touch /work/compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass /work/compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.na -y --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} --version
 
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-npx-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: bash 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm, tzdata, locales..." && 
            export DEBIAN_FRONTEND=noninteractive && apt-get update && apt-get install -y npm tzdata locales-all &&
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 /work/scripts/npx_optional /work/tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass /work/tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.na -y --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} --use-os-locale --version
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-npx-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            tzlc/* 
    

  compatibility_linux_npm_apk:
    name: "Compat NPM ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          # hangs with QEMU https://github.com/nodejs/docker-node/issues/1973
          # - [ 
          #   "arm32v7",  # variant name 
          #   "arm/v7",  # platform spec 
          #   "arm32v7/",  # image prefix 
          # ] 
    runs-on: ubuntu-22.04 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 

      - name: Setup Node for Registry Server
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        if: steps.get_image.outputs.digest != '' 
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: debug, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - 0.0.0.0:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir tzlc 
            
      - name: Download artifacts
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        if: steps.get_image.outputs.digest != '' 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
  
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm..." && 
            apk update && apk add npm &&
            echo "test icloudpd..." && 
            npm install -g --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} &&
            icloudpd --version  &&
            touch /work/compatibility/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 

      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm, tzdata, locales..." && 
            export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
            apk update && apk add npm tzdata musl-locales musl-locales-lang &&
            echo "test icloudpd..." && 
            npm install -g --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} &&
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version 1>.result &&
            cat .result &&
            mv .result /work/tzlc/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass 

      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apt
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
  compatibility_linux_npx_apk:
    name: "Compat NPX ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
        prop:  
          - [ 
            "amd64",  # variant name 
            "amd64",  # platform spec 
            "",  # image prefix 
          ] 
          - [ 
            "arm64",  # variant name 
            "arm64",  # platform spec 
            "",  # image prefix 
          ] 
          # hangs with QEMU https://github.com/nodejs/docker-node/issues/1973
          # - [ 
          #   "arm32v7",  # variant name 
          #   "arm/v7",  # platform spec 
          #   "arm32v7/",  # image prefix 
          # ] 
    runs-on: ubuntu-22.04 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Get image 
        id: get_image  
        run: |  
          echo digest=$(docker pull --platform linux/${{ matrix.prop[1] }} ${{ matrix.prop[2] }}${{ matrix.image[1] }} | grep Digest | head -n 1 | cut -d: -f 2-3 | tr -d ' ') >> $GITHUB_OUTPUT 

      - name: Setup Node for Registry Server
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        if: steps.get_image.outputs.digest != '' 
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: debug, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - 0.0.0.0:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        if: steps.get_image.outputs.digest != '' 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        if: steps.get_image.outputs.digest != '' 
        run: | 
          mkdir tzlc 
            
      - name: Download artifacts
        if: steps.get_image.outputs.digest != '' 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        if: steps.get_image.outputs.digest != '' 
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        if: steps.get_image.outputs.digest != '' 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      # ARM runs natively on ubuntu-22.04-arm runners - no QEMU needed 
  
      - name: Run test for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_test 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm..." && 
            apk update && apk add npm &&
            echo "test icloudpd..." && 
            npx -y --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} --version &&
            touch /work/compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass
 
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-npx-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: steps.get_image.outputs.digest != ''  
        id: run_tzlc 
        uses: addnab/docker-run-action@v3 
        continue-on-error: true 
        with: 
          image: ${{ matrix.prop[2] }}${{ matrix.image[1] }} 
          shell: sh 
          options: -v ${{ github.workspace }}:/work --platform linux/${{ matrix.prop[1] }} 
          run: > 
            echo "install npm, tzdata, locales..." && 
            export MUSL_LOCPATH="/usr/share/i18n/locales/musl" &&
            apk update && apk add npm tzdata musl-locales musl-locales-lang &&
            echo "test icloudpd..." && 
            TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 npx -y --registry http://172.17.0.1:4873 icloudpd@${{inputs.icloudpd_version}} --use-os-locale --version 1>.result &&
            cat .result &&
            mv .result /work/tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.pass
  
      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        if: steps.get_image.outputs.digest != ''  
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-npx-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk
          if-no-files-found: error 
          path: | 
            tzlc/* 
    

  # record QEMU failures for completeness
  compatibility_linux_npm_apk_fail:
    name: "Compat NPM/NPX ${{ matrix.image[1] }} ${{ matrix.prop[1] }} apk failures" 
    strategy:  
      fail-fast: false 
      matrix: 
        image:  
          - [ 
            "alpine_3_22", #variant name
            "alpine:3.22", #image
            ] 
          - [ 
            "alpine_3_21", #variant name
            "alpine:3.21", #image
            ] 
          - [ 
            "alpine_3_20", #variant name - TODO Test
            "alpine:3.20", #image
            ] 
          - [ 
            "alpine_3_19", #variant name
            "alpine:3.19", #image
            ] 
          - [ 
            "alpine_3_18", #variant name
            "alpine:3.18", #image
            ] 
          - [ 
            "alpine_3_17", #variant name
            "alpine:3.17", #image
            ] 
          - [ 
            "alpine_3_16", #variant name
            "alpine:3.16", #image
            ] 
          - [ 
            "alpine_3_15", #variant name
            "alpine:3.15", #image
            ] 
          - [ 
            "alpine_3_14", #variant name
            "alpine:3.14", #image
            ] 
          - [ 
            "alpine_3_13", #variant name
            "alpine:3.13", #image
            ] 
        prop:  
          # - [ 
          #   "amd64",  # variant name 
          #   "amd64",  # platform spec 
          #   "",  # image prefix 
          # ] 
          # - [ 
          #   "arm64",  # variant name 
          #   "arm64",  # platform spec 
          #   "",  # image prefix 
          # ] 
          # hangs with QEMU https://github.com/nodejs/docker-node/issues/1973
          - [ 
            "arm32v7",  # variant name 
            "arm/v7",  # platform spec 
            "arm32v7/",  # image prefix 
          ] 
    runs-on: ubuntu-22.04 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        run: | 
          mkdir tzlc 
  
      - name: Record failure for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        run: | 
          touch compatibility/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
          touch compatibility/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk-fail
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Record failure tzlc for ${{ matrix.prop[2] }}${{ matrix.image[1] }} on ${{ matrix.prop[1] }} 
        run: | 
          touch tzlc/npm.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
          touch tzlc/npx.${{ matrix.image[0] }}.${{ matrix.prop[0] }}.fail 
  
      - name: Upload tzlc result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-linux-npm-${{ matrix.image[0] }}-${{ matrix.prop[0] }}-apk-fail
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
  compatibility_macos_npm:
    name: "Compat NPM ${{ matrix.prop[0] }} ${{ matrix.prop[1] }}" 
    strategy:  
      fail-fast: false 
      matrix: 
        prop:  
          - [ 
            "macos-13", 
            "amd64", 
            ] 
          - [ 
            "macos-14", 
            "arm64", 
            ] 
    runs-on: ${{ matrix.prop[0] }} 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Setup Node for Registry Server
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: http, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - localhost:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        run: | 
          mkdir tzlc             

      - name: Download artifacts
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Run test on ${{ matrix.prop[0] }} 
        id: run_test 
        continue-on-error: true 
        run: > 
          npm install -g --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} &&
          icloudpd --version  &&
          touch compatibility/npm.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
 
      - name: Record failure on ${{ matrix.prop[0] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npm.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-macos-npm-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc on ${{ matrix.prop[0] }} 
        id: run_tzlc 
        continue-on-error: true 
        run: > 
          npm install -g --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} &&
          TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 icloudpd --use-os-locale --version  1>.result &&
          cat .result &&
          mv .result tzlc/npm.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
  
      - name: Record failure tzlc on ${{ matrix.prop[0] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npm.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
  
      - name: Upload tzlc result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-macos-npm-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
  compatibility_macos_npx:
    name: "Compat NPX ${{ matrix.prop[0] }} ${{ matrix.prop[1] }}" 
    strategy:  
      fail-fast: false 
      matrix: 
        prop:  
          - [ 
            "macos-13", 
            "amd64", 
            ] 
          - [ 
            "macos-14", 
            "arm64", 
            ] 
    runs-on: ${{ matrix.prop[0] }} 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Setup Node for Registry Server
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        run: |
          touch ${{ github.workspace }}/npm_config.yaml
          mkdir ${{ github.workspace }}/verdaccio
          echo "
          storage: ${{ github.workspace }}/verdaccio
          max_body_size: 1000mb
          log: { type: file, format: plain, level: http, path: ${{ github.workspace }}/verdaccio.log }
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - localhost:4873" > ${{ github.workspace }}/npm_config.yaml
          cat ${{ github.workspace }}/npm_config.yaml

      - name: Install Registry
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        shell: bash 
        run: verdaccio --config ${{ github.workspace }}/npm_config.yaml &

      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 

      - name: Make folder for tzlc 
        run: | 
          mkdir tzlc 

      - name: Download artifacts
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"
          
      - name: Publish NPM
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Run test on ${{ matrix.prop[0] }} 
        id: run_test 
        continue-on-error: true 
        run: > 
          npx -y --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} --version  &&
          touch compatibility/npx.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
 
      - name: Record failure on ${{ matrix.prop[0] }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npx.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-macos-npx-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            compatibility/* 

      - name: Run tzlc on ${{ matrix.prop[0] }} 
        id: run_tzlc 
        continue-on-error: true 
        run: > 
          TZ=America/Los_Angeles LC_ALL=ru_RU.UTF-8 npx -y --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} --use-os-locale --version 1>.result &&
          cat .result &&
          mv .result tzlc/npx.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.pass 
  
      - name: Record failure tzlc on ${{ matrix.prop[0] }} 
        if: ${{ steps.run_tzlc.outcome == 'failure' }}  
        run: | 
          touch tzlc/npx.${{ matrix.prop[0] }}.${{ matrix.prop[1] }}.fail 
  
      - name: Upload tzlc result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-tzlc-macos-npx-${{ matrix.prop[0] }}-${{ matrix.prop[1] }}
          if-no-files-found: error 
          path: | 
            tzlc/* 
    
  compatibility_windows_npm:
    name: "Compat NPM ${{ matrix.os }} amd64" 
    strategy:  
      fail-fast: false 
      matrix: 
        os: 
          - "windows-2025"
          - "windows-2022"
    runs-on: ${{ matrix.os }} 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Setup Node for Registry Server
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        run: |
          touch npm_config.yaml
          mkdir verdaccio
          echo "
          storage: verdaccio
          log: { type: file, format: plain, level: debug, path: verdaccio.log }
          max_body_size: 1000mb
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - localhost:4873" > npm_config.yaml
          cat npm_config.yaml

      - name: Install Registry 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        shell: bash 
        run: verdaccio --config npm_config.yaml &

      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 
 
      - name: Download artifacts
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Publish NPM
        continue-on-error: true 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Run test on ${{ matrix.os }} 
        id: run_test 
        continue-on-error: true 
        run: > 
          npm install -g --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} &&
          icloudpd --version &&
          touch compatibility/npm.${{ matrix.os }}.amd64.pass 
 
      - name: Record failure on ${{ matrix.os }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npm.${{ matrix.os }}.amd64.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-windows-npm-${{ matrix.os }}-amd64
          if-no-files-found: error 
          path: | 
            compatibility/* 

  compatibility_windows_npx:
    name: "Compat NPX ${{ matrix.os }} amd64" 
    strategy:  
      fail-fast: false 
      matrix: 
        os: 
          - "windows-2025"
          - "windows-2022"
    runs-on: ${{ matrix.os }} 
    needs: [ build_npm ] 
    defaults: 
      run: 
        shell: bash 
 
    steps: 
 
      - name: Setup Node for Registry Server
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'

      - name: Create Config
        run: |
          touch npm_config.yaml
          mkdir verdaccio
          echo "
          storage: verdaccio
          log: { type: file, format: plain, level: debug, path: verdaccio.log }
          max_body_size: 1000mb
          packages:
            '**':
              access: \$anonymous
              publish: \$anonymous
          listen:
            - localhost:4873" > npm_config.yaml
          cat npm_config.yaml

      - name: Install Registry 
        shell: bash 
        run: npm install -g verdaccio@5

      - name: Start Registry 
        shell: bash 
        run: verdaccio --config npm_config.yaml &

      - name: Make folder for compatibility 
        run: | 
          mkdir compatibility 
 
      - name: Download artifacts
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-npm
          path: | 
            dist/npm

      - name: Setup Node for Test
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'http://localhost:4873'
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Publish NPM
        continue-on-error: true 
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/linux-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/win32-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-x64 --access public --registry http://localhost:4873
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public --registry http://localhost:4873
          npm publish dist/npm/icloudpd --access public --registry http://localhost:4873
        env:
          NODE_AUTH_TOKEN: "fake"

      - name: Run test on ${{ matrix.os }} 
        id: run_test 
        continue-on-error: true 
        run: > 
          npx -y --registry http://localhost:4873 icloudpd@${{inputs.icloudpd_version}} --version  &&
          touch compatibility/npx.${{ matrix.os }}.amd64.pass 
 
      - name: Record failure on ${{ matrix.os }} 
        if: ${{ steps.run_test.outcome == 'failure' }}  
        run: | 
          touch compatibility/npx.${{ matrix.os }}.amd64.fail 
 
      - name: Upload compatibility result 
        uses: actions/upload-artifact@v4 
        with: 
          name: icloudpd-compatibility-windows-npx-${{ matrix.os }}-amd64
          if-no-files-found: error 
          path: | 
            compatibility/* 

  compatibility_report:
    name: "Build Compatibility Report"

    runs-on: ubuntu-22.04
    needs: 
      - compatibility_macos_pip
      - compatibility_windows_pip
      - compatibility_linux_pip_apt
      - compatibility_linux_pip_apk
      - compatibility_linux_bin_apt
      - compatibility_linux_bin_apk
      - compatibility_linux_bin_musl_apt
      - compatibility_linux_bin_musl_apk
      - compatibility_macos_bin
      - compatibility_windows_bin
      - compatibility_docker
      - compatibility_linux_npm_apt
      - compatibility_linux_npx_apt
      - compatibility_linux_npm_apk
      - compatibility_linux_npx_apk
      - compatibility_linux_npm_apk_fail
      - compatibility_macos_npm
      - compatibility_macos_npx
      - compatibility_windows_npm
      - compatibility_windows_npx
      - get_expected_version_linux_apt
      - get_expected_version_linux_apk
      - get_expected_version_macos
      - get_version_thumbprint
    defaults:
      run:
        shell: bash

    steps:

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'
    
      - name: Make folder for artifacts
        run: |
          mkdir dist

      - name: Download Compatibility Results
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-compatibility-*
          merge-multiple: true
          path: |
            compatibility

      - name: Download tzlc Results
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-tzlc-*
          merge-multiple: true
          path: |
            tzlc

      - name: Download Version
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-version-thumbprint
          merge-multiple: true
          path: |
            dist

      - name: Compile Compatibility Report
        run: |
          echo "save report..."
          scripts/compile_compatibility.py dist/icloudpd-version-thumbprint.txt compatibility | tee dist/compatibility-${{inputs.icloudpd_version}}.md

      - name: Upload compatibility report
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-compatibility
          if-no-files-found: error
          path: |
            dist/compatibility-${{inputs.icloudpd_version}}.md

      - name: Compile tzlc Report
        run: |
          echo "save report..."
          scripts/compile_tzlc.py dist/icloudpd-version-thumbprint.txt tzlc "${{needs.get_expected_version_linux_apt.outputs.expected_version}}" "${{needs.get_expected_version_linux_apk.outputs.expected_version}}" "${{needs.get_expected_version_macos.outputs.expected_version}}" | tee dist/tzlc-${{inputs.icloudpd_version}}.md

      - name: Upload tzlc report
        uses: actions/upload-artifact@v4
        with:
          name: icloudpd-tzlc
          if-no-files-found: error
          path: |
            dist/tzlc-${{inputs.icloudpd_version}}.md
    

================================================
FILE: .github/workflows/codeql-analysis.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
# ******** NOTE ********

name: "CodeQL"

on:
  push:
    branches: [ master ]
  pull_request:
    # The branches below must be a subset of the branches above
    branches: [ master ]
  schedule:
    - cron: '40 3 * * 1'

jobs:
  analyze:
    name: Analyze
    runs-on: ubuntu-22.04

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]
        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
        # Learn more...
        # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v2
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.
        # queries: ./path/to/local/query, your-org/your-repo/queries@main

    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v2

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 https://git.io/JvXDl

    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
    #    and modify them (or add more) to build your code if your project
    #    uses a compiled language

    #- run: |
    #   make bootstrap
    #   make release

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v2


================================================
FILE: .github/workflows/compile-notes.yml
================================================

name: Compile Release Notes

on:
  workflow_call:
    inputs:
      icloudpd_version:
        required: true
        type: string

jobs:

  build_notes:
    runs-on: ubuntu-22.04
    steps:

    - name: Download artifacts (compatibility)
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-compatibility
        path: |
          dist

    - name: Download artifacts (changelog)
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-changelog
        path: |
          dist

    - name: Merge Release Notes
      run: |
        cat dist/changelog-*.md > dist/notes-${{inputs.icloudpd_version}}.md
        cat dist/compatibility-*.md >> dist/notes-${{inputs.icloudpd_version}}.md

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-notes
        if-no-files-found: error
        path: |
          dist/notes-${{inputs.icloudpd_version}}.md


================================================
FILE: .github/workflows/create-release.yml
================================================
on:
  push:
    # Sequence of patterns matched against refs/tags
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10

name: Create Release

jobs:
  get_version:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4

    - name: Retrieve version
      id: get_version
      run: |
        echo icloudpd_version=$(cat pyproject.toml | grep version= | cut -d'"' -f 2) >> $GITHUB_OUTPUT

    - name: Log version
      run: |
        echo "icloudpd_version=${{steps.get_version.outputs.icloudpd_version}}"

    outputs:
      icloudpd_version: ${{steps.get_version.outputs.icloudpd_version}}

  extract_changelog:
    needs: [get_version]
    uses: ./.github/workflows/extract-changelog.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

  patch_version:
    uses: ./.github/workflows/patch-version.yml
    
  build_package:
    needs: [get_version, patch_version]
    uses: ./.github/workflows/build-package.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

  compile_notes:
    needs: [get_version, build_package, extract_changelog]
    uses: ./.github/workflows/compile-notes.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

  publish:
    needs: [get_version, build_package, compile_notes]
    uses: ./.github/workflows/publish.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}
      PYPI_REPOSITORY: ${{ vars.PYPI_REPOSITORY }}
    secrets:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_PASSWORD: ${{ secrets.DOCKERHUB_PASSWORD }}
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
      PYPI_USERNAME: ${{ secrets.PYPI_USERNAME }}
      PYPI_PASSWORD: ${{ secrets.PYPI_PASSWORD }}


================================================
FILE: .github/workflows/extract-changelog.yml
================================================

name: Extract Changelog

on:
  workflow_call:
    inputs:
      icloudpd_version:
        required: true
        type: string

jobs:

  extract_changelog:
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4

    - name: Make dist folder
      run: |
        mkdir -p dist

    - name: Retrieve Changelog
      id: get_change_log
      run: |
        scripts/extract_releasenotes CHANGELOG.md > dist/changelog-${{inputs.icloudpd_version}}.md

    - name: Log Changelog
      run: |
        cat dist/changelog-${{inputs.icloudpd_version}}.md
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-changelog
        if-no-files-found: error
        path: |
          dist/changelog-*.md


================================================
FILE: .github/workflows/patch-version.yml
================================================

name: Patch Version

on:
  workflow_call:

jobs:

  patch_version:
    runs-on: ubuntu-22.04
    steps:

    - uses: actions/checkout@v4 

    - name: Patch Version
      run: |
        scripts/patch_version

    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: icloudpd-version-info
        if-no-files-found: error
        path: |
          src/foundation/__init__.py


================================================
FILE: .github/workflows/produce-artifacts.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Produce Artifacts

on: 
  pull_request:
    # branches: [ master ]
  workflow_dispatch:
    # branches:
    #   - '**'


jobs:
  skip_check:
    # continue-on-error: true # Uncomment once integration is finished
    runs-on: ubuntu-22.04
    # Map a step output to a job output
    outputs:
      should_skip: ${{ steps.skip_check.outputs.should_skip }}
    steps:
      - id: skip_check
        uses: fkirc/skip-duplicate-actions@v5
        with:
          concurrent_skipping: 'same_content_newer'
          skip_after_successful_duplicate: 'true'
          paths_ignore: '["**/*.md", "examples/**", "tests/**", "docs/**"]'
          do_not_skip: '["workflow_dispatch", "schedule"]'

  get_version:
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4

    - name: Retrieve version
      id: get_version
      run: |
        echo icloudpd_version=$(cat pyproject.toml | grep version= | cut -d'"' -f 2) >> $GITHUB_OUTPUT

    - name: Log version
      run: |
        echo "icloudpd_version=${{steps.get_version.outputs.icloudpd_version}}"

    outputs:
      icloudpd_version: ${{steps.get_version.outputs.icloudpd_version}}

  extract_changelog:
    needs: [get_version]
    uses: ./.github/workflows/extract-changelog.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

  patch_version:
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    uses: ./.github/workflows/patch-version.yml

  build_package:
    needs: [get_version, patch_version]
    if: needs.skip_check.outputs.should_skip != 'true'
    uses: ./.github/workflows/build-package.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

  compile_notes:
    needs: [get_version, build_package, extract_changelog]
    if: needs.skip_check.outputs.should_skip != 'true'
    uses: ./.github/workflows/compile-notes.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}


================================================
FILE: .github/workflows/publish-docs.yml
================================================
name: "Publish docs to GH pages"

on: workflow_call

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    name: Deploy
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

================================================
FILE: .github/workflows/publish.yml
================================================
on:
  workflow_call:
    inputs:
      icloudpd_version:
        required: true
        type: string
      PYPI_REPOSITORY:
        required: false
        type: string
    secrets:
      DOCKERHUB_USERNAME:
        required: false
      DOCKERHUB_PASSWORD:
        required: false
      NPM_TOKEN:
        required: false
      PYPI_USERNAME:
        required: false
      PYPI_PASSWORD:
        required: false

name: Publish Release

jobs:

  docker:
    runs-on: ubuntu-22.04

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Download artifacts 
        uses: actions/download-artifact@v4 
        with: 
          name: icloudpd-oci
          path: | 
            dist 
 
      - name: Run skopeo 
        uses: addnab/docker-run-action@v3 
        with: 
          image: quay.io/skopeo/stable:v1.14.0 
          shell: sh 
          options: -v ${{ github.workspace }}/dist:/dist 
          run: > 
            (skopeo copy --preserve-digests --dest-creds=${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_PASSWORD }} --all oci-archive:/dist/icloudpd-${{inputs.icloudpd_version}}-oci.tar docker://docker.io/icloudpd/icloudpd:${{inputs.icloudpd_version}}) &&
            (skopeo copy --preserve-digests --src-creds=${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_PASSWORD }} --dest-creds=${{ secrets.DOCKERHUB_USERNAME }}:${{ secrets.DOCKERHUB_PASSWORD }} --all docker://docker.io/icloudpd/icloudpd:${{inputs.icloudpd_version}} docker://docker.io/icloudpd/icloudpd:latest)
  
      - name: Update repo description
        uses: peter-evans/dockerhub-description@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_PASSWORD }}
          repository: icloudpd/icloudpd
          readme-filepath: ./README_DOCKER.md
          short-description: ${{ github.event.repository.description }}

  npm:
    runs-on: ubuntu-22.04
    steps:
      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20.x'
          registry-url: 'https://registry.npmjs.org'

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-npm
          path: |
            dist/npm

      - name: Publish NPM
        run: |
          npm publish dist/npm/@icloudpd/linux-arm --access public
          npm publish dist/npm/@icloudpd/linux-arm64 --access public
          npm publish dist/npm/@icloudpd/linux-x64 --access public
          npm publish dist/npm/@icloudpd/win32-x64 --access public
          npm publish dist/npm/@icloudpd/darwin-x64 --access public
          npm publish dist/npm/@icloudpd/darwin-arm64 --access public
          npm publish dist/npm/icloudpd --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  pypi:
    runs-on: ubuntu-22.04
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python 3.13
        uses: actions/setup-python@v5
        with:
          python-version: '3.13'
      
      - name: Install Dev dependencies
        run: |
          pip3 install --group dev

      - name: Download artifacts (src)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-src
          path: |
            dist

      - name: Download artifacts (bin)
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-bin-*
          merge-multiple: true
          path: |
            dist

      - name: Publish PyPI
        env:
          TWINE_REPOSITORY: ${{ inputs.PYPI_REPOSITORY }}
          TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }}
          TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }}
        run: |
          python3 -m twine upload --non-interactive --disable-progress-bar dist/*.whl

  gh_release:
    runs-on: ubuntu-22.04
    steps:

      - name: Download artifacts (src)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-src
          path: |
            dist

      - name: Download artifacts (bin)
        uses: actions/download-artifact@v4
        with:
          pattern: icloudpd-bin-*
          merge-multiple: true
          path: |
            dist

      - name: Download artifacts (docker)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-oci
          path: |
            dist

      - name: Download artifacts (compatibility)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-compatibility
          path: |
            dist

      - name: Download artifacts (tzlc compatibility)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-tzlc
          path: |
            dist

      - name: Download artifacts (notes)
        uses: actions/download-artifact@v4
        with:
          name: icloudpd-notes
          path: |
            dist

      - name: Create Release
        uses: ncipollo/release-action@v1
        with:
          artifacts: "dist/*"
          bodyFile: dist/notes-${{inputs.icloudpd_version}}.md



================================================
FILE: .github/workflows/quality-checks.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Quality Checks

on:
  push:
    branches:
      - '**'
  pull_request:
    # branches: [ master ]
  workflow_dispatch:

jobs:
  skip_check:
    # continue-on-error: true # Uncomment once integration is finished
    runs-on: ubuntu-22.04
    # Map a step output to a job output
    outputs:
      should_skip: ${{ steps.skip_check.outputs.should_skip }}
    steps:
      - id: skip_check
        uses: fkirc/skip-duplicate-actions@v5
        with:
          concurrent_skipping: 'same_content_newer'
          skip_after_successful_duplicate: 'true'
          paths_ignore: '["**/*.md", "examples/**", "docs/**"]'
          do_not_skip: '["workflow_dispatch", "schedule"]'

  lint:
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ubuntu-22.04
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps:
    - uses: actions/checkout@v4
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
    - name: Install Test dependencies
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check . --group test

    - name: Lint
      run: |
        scripts/lint

  type_check: 
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ubuntu-22.04
    strategy: 
      matrix: 
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps: 
    - uses: actions/checkout@v4 
    - name: Set up Python ${{ matrix.python-version }} 
      uses: actions/setup-python@v5 
      with: 
        python-version: ${{ matrix.python-version }} 
        cache: 'pip' 
    - name: Install Test dependencies 
      run: > 
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check . --group test
    - name: Type Check 
      run: | 
        scripts/type_check 

  patch_version:
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    uses: ./.github/workflows/patch-version.yml

  test: 
    needs: [skip_check, patch_version]
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ubuntu-22.04
    strategy: 
      matrix: 
        python-version: ['3.10', '3.11', '3.12', '3.13']
    steps: 
    - name: Install Locales for Tests
      run: | 
        sudo apt-get update && sudo apt-get -y install locales locales-all

    - uses: actions/checkout@v4 

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python ${{ matrix.python-version }} 
      uses: actions/setup-python@v5 
      with: 
        python-version: ${{ matrix.python-version }} 
        cache: 'pip' 
    
    - name: Install Test dependencies 
      run: >
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt && 
        pip3 install --disable-pip-version-check . --group test
 
    - name: Test
      run: |
        scripts/test

  test_non_linux: 
    needs: [skip_check, patch_version]
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ${{ matrix.os }} 
    defaults:
      run:
        shell: bash
    strategy: 
      fail-fast: false
      matrix: 
        python-version: [3.13]
        os: 
          - "macos-13"
          - "macos-14"
          - "windows-2025"
          - "windows-2022"
    steps: 

    - uses: actions/checkout@v4 

    - name: Download version info
      uses: actions/download-artifact@v4
      with:
        name: icloudpd-version-info
        path: |
          src/foundation

    - name: Set up Python ${{ matrix.python-version }} 
      uses: actions/setup-python@v5 
      with: 
        python-version: ${{ matrix.python-version }} 
        cache: 'pip' 
    
    - name: Install Test dependencies 
      run: > 
        python3 -m pip install --disable-pip-version-check -r requirements-pip.txt &&
        pip3 install --disable-pip-version-check . --group test
 
    - name: Test
      run: |
        scripts/test

  get_version:
    needs: skip_check
    if: needs.skip_check.outputs.should_skip != 'true'
    runs-on: ubuntu-22.04
    steps:
    - uses: actions/checkout@v4

    - name: Retrieve version
      id: get_version
      run: |
        echo icloudpd_version=$(scripts/get_version) >> $GITHUB_OUTPUT

    - name: Log version
      run: |
        echo "icloudpd_version=${{steps.get_version.outputs.icloudpd_version}}"

    outputs:
      icloudpd_version: ${{steps.get_version.outputs.icloudpd_version}}

  extract_changelog:
    needs: [get_version, skip_check]
    if: needs.skip_check.outputs.should_skip != 'true'
    uses: ./.github/workflows/extract-changelog.yml
    with:
      icloudpd_version: ${{needs.get_version.outputs.icloudpd_version}}

        

================================================
FILE: .github/workflows/refresh-docs.yml
================================================
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions

name: Refresh Docs

on:
  push:
    tags:
      - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
  workflow_dispatch:

jobs:
  build_docs:
    uses: ./.github/workflows/build-docs.yml

  publish_docs:
    needs: [build_docs]
    uses: ./.github/workflows/publish-docs.yml


================================================
FILE: .gitignore
================================================
.DS_Store
build/
dist/
wheelhouse/
icloudpd.egg-info
tests/fixtures/**
__pycache__
.pytest_cache
.cache
*.pyc
.idea
.coverage
.coverage.*
htmlcov
*.whl
.venv
venv
*.spec
# docs
docs/_build
# exclude since there is no js development, just for local testing npm packaging
node_modules/
/package.json
/package-lock.json
# used for local troubleshooting
.photos
.cookies/

.claude/


================================================
FILE: .vscode/settings.json
================================================
{
    "cSpell.words": [
        "icloudpd",
        "seealso",
        "versionadded",
        "versionchanged"
    ]
}

================================================
FILE: CHANGELOG.md
================================================
# Change log

## Unreleased

## 1.32.2 (2025-09-01)

- fix: HTTP response content not captured for authentication and non-streaming requests [#1240](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1240)
- fix: `--only-print-filenames` downloads live photo video files during deduplication [#1220](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1220)

## 1.32.1 (2025-08-30)

- fix: KeyError when downloading photos with --size adjusted --size alternative options [#926](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/926)

## 1.32.0 (2025-08-29)

- feat: support multiple user configurations in single command [#1067](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1067) [#923](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/923)

## 1.31.0 (2025-08-20)

- fix: OOMing on large downloads [#1214](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1214)
- feat: support downloading from multiple albums [#738](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/738) [#438](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/438)

## 1.30.0 (2025-08-17)

- feat: resume interrupted downloads [#968](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/968) [#793](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/793)
- feat: add `--skip-photos` filter [#401](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/401) [#1206](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1206)

## 1.29.4 (2025-08-12)

- fix: failed auth terminate webui [#1195](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1195)
- fix: disable raw password auth fallback [#1176](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1176)

## 1.29.3 (2025-08-09)

- debug: dump auth traffic in case of a failure [#1176](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1176)
- fix: transfer chunk issues terminate with error [#1202](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1202)

## 1.29.2 (2025-07-21)

- fix: emulating old browser and client version fails authentication [#1073](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1073)

## 1.29.1 (2025-07-20)

- fix: retries trigger rate limiting [#1195](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1195)
- fix: smtp & script notifications terminate the program [#898](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/898)

## 1.29.0 (2025-07-19)

- fix: connection errors reported with stack trace [#1187](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1187)
- feat: `--skip-created-after` to limit assets by creation date [#466](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/466) [#1111](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1111)
- fix: connecting to a non-activated iCloud service reported as an error
- fix: 503 response reported as an error [#1188](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1188)

## 1.28.2 (2025-07-06)

- chore: bump min python version 3.9->3.10
- fix: iCloud clean up with `--keep-icloud-recent-days` does not respect `--skip-*` params [#1180](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1180) 
- chore: replace build & test platform from retired windows-2019 to windows-2025
- Service Temporary Unavailable responses are less ambiguous [#1078](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1078)
- feat: re-authenticate on errors when using `--watch-with-interval` [#1078](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1078)
- feat: use stored cookies before attempting to authenticate with credentials

## 1.28.1 (2025-06-08)

- fix: UserWarning about obsoleted code [#1148](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1144) [#1142](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1148)

## 1.28.0 (2025-06-02)

- feat: `--skip-created-before` to limit assets by creation date [#466](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/466) [#1111](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1111)
- bug: `--watch-with-interval` does not process updates from iCloud [#1144](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1144) [#1142](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1142)

## 1.27.5 (2025-05-08)

- fix: HEIF file extension [#1133](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1133)
- fix: fix ignored photos with --delete-after-download or --keep-icloud-recent-days [#616](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/616)
- fix: timeout set to 30 seconds for HTTP requests [#793](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/793)

## 1.27.4 (2025-04-15)

- fix: broken pypi publishing [#1105](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1105)

## 1.27.3 (2025-04-14)

- feature: the icloud email username is now included in the email about 2sa authentication failing, for when an installation is configured for multiple icloud accounts.

## 1.27.2 (2025-03-29)

- fix: dates prior 1970 do not work on non linux [#1045](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1045)

## 1.27.1 (2025-03-16)

- fix: disambiguate whole photo collection from the album with `All Photos` name [#1077](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1077)

## 1.27.0 (2025-02-22)

- feature: list and download from shared libraries for invitee [#947](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/947)

## 1.26.1 (2025-01-23)

- fix: XMP metadata with plist in XML format causes crash [#1059](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1059)
- fix: missing 'isFavorite' in XMP metadata causes crash [#1058](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1058)
- fix: crash when downloading files with `--xmp-sidecar` caused by files having non-JSON adjustment data. [#1056](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1056)

## 1.26.0 (2025-01-13)

- feature: add `--keep-icloud-recent-days` parameter to keep photos newer than this many days in iCloud. Deletes the rest. [#1046](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1046)

## 1.25.1 (2024-12-28)

- chore: bump max/default python version 3.12->3.13
- chore: bump min python version 3.8->3.9
- fix: fallback to old raw password auth if srp auth fails [#975](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/975)

## 1.25.0 (2024-12-03)

- fix: failed to authenticate for accounts with srp s2k_fo auth protocol [#975](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/975)
- fix: failed to login non-2FA account for the first attempt [#1012](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1012)
- fix: log more information for authentication error [#1010](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1010)
- feature: add support for XMP files with `--xmp-sidecar` parameter [#448](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/448), [#102](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/102), [#789](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/789)

## 1.24.4 (2024-11-18)

- fix: deprecate macos-12 [#1000](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/1000)
- fix: sms MFA dropping leading zeros [#993](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/993)

## 1.24.3 (2024-11-03)

- fix: crashes when no imagetype sent by Apple [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/680)

## 1.24.2 (2024-11-02)

- fix: errors for accounts with salt started with zero byte [#975](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/975)

## 1.24.1 (2024-10-28)

- fix: accounts without 2fa are supported [#959](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/959)

## 1.24.0 (2024-10-25)

- fix: new AppleID auth with srp [#970](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/970)
- feature: when ran without parameters, `icloudpd` shows help [#963](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/963)
- fix: force_size should not skip subsequent sizes [#955](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/955)

## 1.23.4 (2024-09-02)

- fix: support plain text encoding for filename in addition to base64 [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/641)

## 1.23.3 (2024-09-01)

- more debug added for parsing filenameEnc [#935](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/935) and [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/641)

## 1.23.2 (2024-08-31)

- dump encoded filename in exception when there is an error in decoding it [#935](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/935) and [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/641)

## 1.23.1 (2024-08-22)

- fix: use a-z for sms mfa index to disambiguate with mfa code with leading zeros [#925](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/925)
- fix: report proper error on bad `--folder-structure` value [#937](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/937)

## 1.23.0 (2024-07-25)

- feature: update webui and allow to cancel and resume sync
- deprecate linux 386 and arm v6 support
- add linux musl builds

## 1.22.0 (2024-07-12)

- feature: support for using locale from OS with `--use-os-locale` flag [#897](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/897)
- fix: swallow keyring errors [#871](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/871)

## 1.21.0 (2024-07-05)

- feature: add webui for entering password with `--password-provider webui` parameter [#805](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/805)
- feature: add webui for entering MFA code with `--mfa-provider webui` parameter [#805](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/805)
- fix: allow MFA with leading zeros [ref](https://github.com/boredazfcuk/docker-icloudpd/issues/599)

## 1.20.4 (2024-06-30)

- fix: SMS MFA [#803](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/803)

## 1.20.3 (2024-06-29)

- fix: release to PyPi [#883](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/883)

## 1.20.2 (2024-06-28)

- fix: match SMS MFA to icloud.com behavior [#803](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/803)

## 1.20.1 (2024-06-18)

- fix: keyring handling in `icloud` [#871](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/871)

## 1.20.0 (2024-06-16)

- feature: customize choice and the order of checking for password with `--password-provider` parameter
- feature: support multiple file naming and de-deplication policies with `--file-match-policy` parameter. Rel to [#346](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/346)

## 1.19.1 (2024-06-02)

- fix: KeyError alternative [#859](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/859)

## 1.19.0 (2024-05-31)

- fix: release notes [#849](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/849)
- fix: auto deletion when `--folder-structure` is set to `none` [#831](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/831)
- fix: Apple/Adobe DNG raw photos are recognised as images [#662](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/662)
- feature: support multiple `--size` parameter specifications in command line
- fix: file extensions for non-original version matching type of the asset in the version
- feature: support downloading adjusted files with `--size adjusted` parameter (portraits, edits, etc) with fallback to `original` [#769](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/769) [#704](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/704) [#350](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/350) [#249](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/249)
- feature: support for CR2,CR3,CRW,ARW,RAF,RW2,NRF,PEF,NEF,ORF raw image formats [#675](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/675)
- feature: support `--size alternative` for alternative originals, e.g. raw+jpeg, with fallback to `original` [#675](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/675)
- feature: add `--align-raw` param to treat raw in raw+jpeg as original, alternative (jpeg+raw), or as-is, default to as-is

## 1.18.0 (2024-05-27)

- feature: add parameter `--live-photo-mov-filename-policy` to control naming of video portion of live photos with default `suffix` for compatibility [#500](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/500)
- feature: add parameter `--keep-unicode-in-filenames` with default `false` for compatibility [#845](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/845)
- fix: avoid parsing json from empty responses [#837](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/837)

## 1.17.7 (2024-05-25)

- fix: keyring exception [#841](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/841)
- fix: delete iCloud asset in respective shared library [#802](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/802)

## 1.17.6 (2024-05-23)

- fix: missing exception [#836](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/836)

## 1.17.5 (2024-04-27)

- experimental: fix errors in npm packages
- fix: allow calls for trusted devices to fail silently [#819](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/819)

## 1.17.4 (2024-04-10)

- fix: restore support for SMS MFA [#803](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/803)

## 1.17.3 (2024-01-03)

- improve compatibility for diffeent platforms [#748](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/748)

## 1.17.2 (2023-12-22)

- fix: module not found [#748](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/748)

## 1.17.1 (2023-12-20)

- fix: main macos binary failing [#668](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/668) [#700](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/700)
- fix: debian glibc error [#741](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/741)

## 1.17.0 (2023-12-19)

- fix: macos binary failing [#668](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/668) [#700](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/700)
- fix: 'Invalid email/password combination' exception due to recent iCloud changes [#729](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/729)
- feature: `--auth-only` parameter to independently create/validate session tokens without listing/downloading photos
- feature: 2FA validation merged from `pyicloud`

## 1.16.3 (2023-12-04)

- fix: file date attribute [#714](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/714)
- fix: `icloud --username` parameter reported as not an option [#719](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/719)

## 1.16.2 (2023-09-30)

- fix: send logs to stdout [#697](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/697)

## 1.16.1 (2023-09-27)

- fix: shared libraries throw INTERNAL_ERROR for some users [#690](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/690)

## 1.16.0 (2023-09-25)

- feature: shared library support with `--list-libraries` and `--library` parameters [#455](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/455), [#489](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/489), [#678](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/678)

## 1.15.1 (2023-07-16)

- fix: excessive logging for existing and deduplicated files
- fix: add missing docker platforms back

## 1.15.0 (2023-07-16)

- fix: logs when progress bar enabled
- feature: `--dry-run` parameter to run icloudpd without changes to local files and iCloud
- fix: pypi.org license and description

## 1.14.5 (2023-07-06)

- fix: pypi publishing for macos

## 1.14.4 (2023-07-06)

- fix: docker auth during publishing

## 1.14.3 (2023-07-06)

- add binary wheel without dependencies to pypi
- fix: remove tests from pypi distributions

## 1.14.2 (2023-07-03)

- fix: finite retry on unhandled errors during photo iteration [#642](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/642)
- fix: retry on internal error during deletion [#615](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/615)

## 1.14.1 (2023-07-02)

- fix: retry authN on session error during deletion [#647](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/647)

## 1.14.0 (2023-07-01)

- fix: auto-delete date mismatch [#345](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/345)
- fix: `--version` parameter

## 1.13.4 (2023-06-14)

- experimental: fix npm packaging

## 1.13.4 (2023-06-11)

- experimental: fix npm registry publishing

## 1.13.2 (2023-06-10)

- experimental: fix npm registry publishing

## 1.13.1 (2023-06-10)

- experimental: add support for distributing `icloudpd` with [npm](README_NPM.md) package manager

## 1.13.0 (2023-04-21)

- fix: only delete files successfully downloaded [#614](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/614)

## 1.12.0 (2023-03-10)

- experimental: add macos binary [#551](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/551)
- fix: add `icloud` script to the source distribution [#594](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/594)

## 1.11.0 (2023-02-24)

- feature: add experimental mode for new cli

## 1.10.0 (2023-02-17)

- feature: add `--watch-with-interval` parameter [#568](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/568)
- fix: allow spaces in filenames [#378](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/378)
- feature: add `--notification-email-from` parameter [#496](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/496)

## 1.9.0 (2023-02-10)

- fix: replace invalid chars in filenames with '_' [#378](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/378)
- feature: add `--domain` parameter to support mainland China [#572](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/572), [#545](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/545)
- feature: add `linux/arm/v7` and `linux/arm/v6` docker image [#434](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/434)

## 1.8.1 (2023-02-03)

- fix: avoid crash when downloading over legacy `-original` name [#338](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/338)
- fix: remove mac binary unitl Apple signing is supported [#551](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/551)
- fix: PyPI distribution [#549](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/549)
- fix: keyring error [#539](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/539)

## 1.8.0 (2023-01-27)

- update dependencies to solve [#539](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/539)
- feature: a new command line option `--delete-after-download` to allow user to delete photos in the iCloud right after download is complete. [#431](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/431), [#368](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/368) [#314](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/314) [#124](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/124) [#332](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/332)

## 1.7.3 (2023-01-20)

- deprecating python 3.6
- experimental: package `icloudpd` & `icloud` as executables [#146](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/146)

## 1.7.2 (2021-01-16)

- fix: smtp server_hostname cannot be an empty [#227](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/227)
- fix: Warning for missing `tzinfo` in Docker image removed by adding `tzinfo`-package.
[#286](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/286)

## 1.7.1 (2020-11-15)

- fix: dev Docker build on Windows correctly manages crlf for scripts

## 1.7.1rc1 (2020-11-10)

- fix: --only-print-filenames option displays filenames (live photos) of files that have already been downloaded #200
- fix: docker works on Windows #192

## 1.7.0 (2020-11-1)

- fix: --log-level option [#194](https://github.com/icloud-photos-downloader/icloud_photos_downloader/pull/194)
- feature: Folder structure can be set to 'none' instead of a date pattern,
so all photos will be placed directly into the download directory.
- fix: Empty directory structure being created #185
- feature: removed multi-threaded downloading and added deprecation notice to --threads-num parameter #180, #188
- fix: documentation issues, first addressed in #141 and separated contribution
info from README.md into CONTRIBUTING.md

## 1.6.2 (2020-10-23)

- Began recording updates in `CHANGELOG.md`
- fix: reduce chances of IOErrors by changing default --threads_num to 1 #155, #163
- fix: reduce chances of errors due to missing required parameters #175
- fix: missing downloading process by upgrading tqdm dependency #167

--------------------------------------------

## Earlier Versions

Please refer to the commit history in GitHub:
<https://github.com/icloud-photos-downloader/icloud_photos_downloader/commits/master>


================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
nationality, personal appearance, race, religion, or sexual identity and
orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
  address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team by opening an [issue](https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues/new). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.

Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [http://contributor-covenant.org/version/1/4][version]

[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing iCloud Photos Downloader

[//]: # (inspired from https://raw.githubusercontent.com/keepassxreboot/keepassxc/develop/.github/CONTRIBUTING.md)

:+1::tada: First off, thanks for taking the time to contribute! :tada::+1:

We'd love your contributions to iCloud Photos Downloader. You don't have to know how to code to be able to help!

Please review the following guidelines before contributing.  Also, feel free to propose changes to these guidelines by updating this file and submitting a pull request.

## Table of contents

[How can I contribute?](#how-can-i-contribute)

* [Feature requests](#feature-requests)
* [Bug reports](#bug-reports)
* [Discuss with the team](#discuss-with-the-team)
* [Your first code contribution](#your-first-code-contribution)
* [Pull request process](#pull-request-process)

[Setting up the development environment](#setting-up-the-development-environment)

[How to write a unit test](#how-to-write-a-unit-test)

Please note we have a [Code of Conduct](CODE_OF_CONDUCT.md), please follow it in all your interactions with the project.

Chore:
- [How to release](#how-to-release)


## How can I contribute?

There are several ways to help this project. Let us know about missing features, or report errors. You could even help others by responding to questions about using the project in the [issue tracker on GitHub][issues-section].

### Feature requests

We're always looking for suggestions to improve our application. If you have a suggestion to improve an existing feature, or would like to suggest a completely new feature, please use the [issue tracker on GitHub][issues-section].

### Bug reports

Our software isn't always perfect, but we strive to always improve our work. You may file bug reports in the issue tracker.

Before submitting a bug report, check if the problem has already been reported. Please refrain from opening a duplicate issue. If you want to add further information to an existing issue, simply add a comment on that issue.

### Discuss with the team

When contributing to this repository, please first discuss the change you wish to make via issue,
email, or any other method with the owners of this repository before making a change.

### Your first code contribution

Unsure where to begin contributing to this project? You can start by looking through these `good first issue` and `help-wanted` issues:

* [Good first issues](good+first+issue) – issues which should only require a few lines of code, and a test or two.
* ['Help wanted' issues](help-wanted) – issues which should be a bit more involved than `beginner` issues.

Both issue lists are sorted by total number of comments. While not perfect, looking at the number of comments on an issue can give a general idea of how much an impact a given change will have.

## Pull Request Process

There are some requirements for pull requests:

* All bug fixes should be covered (before/after scenario) with a corresponding
  unit test, refer to [How to write a unit test](#how-to-write-a-unit-test) All other tests pass. Run `./scripts/test`
* 100% test coverage also for new features is expected.
  * After running `./scripts/test`, you will see the test coverage results in the output
  * You can also open the HTML report at: `./htmlcov/index.html`
* Code is formatted with [ruff](https://github.com/astral-sh/ruff). Run `./scripts/format`
* No lint errors. Run `./scripts/lint`
* If you've added or changed any command-line options,
  please update the [Usage](README.md#usage) section in the README.md.
* Make sure your change is documented in the
[Unreleased](CHANGELOG.md#unreleased) section in the CHANGELOG.md.
* We aim to push out a Release once a week (Fridays), if there is at least one new change in CHANGELOG.

If you need to make any changes to the `pyicloud` library,
`icloudpd` uses a fork of this library that has been added as a subfolder `pyicloud_ipd`.

## Setting up the development environment

### Dev Containers

Easy way to isolate development from the rest of host system is by using Docker containers (devcontainers). VS Code & GitHub Codespaces support this workflow and repository is configured for their use.

VS Code supports local devcontainers (running on the same host as VS Code; require Docker on the host, obviously) as well as remote ones.

### Install Python dependencies

``` sh
scripts/install_deps
```

Installs project for editing mode (install all dev and test dependencies too). You can use `icloudpd` script from terminal as well.

### Formatting Python code

``` sh
scripts/format
```

### Validating app behavior

Run lint & tests:

``` sh
scripts/lint
```

``` sh
scripts/test
```

### Building packages

Building Python wheel (with python source files):

``` sh
scripts/build
```

Building platform executables:

``` sh
scripts/build_bin1 icloudpd
scripts/build_bin1 icloud
```
Note: that command is for Linux, including devcontainers. Windows & macOS scripts must be executed on respective platforms.

Building Linux static executables:

``` sh
scripts/build_static icloudpd
scripts/build_static icloud
```

Building Python wheels (with executable):

``` sh
scripts/build_whl
```
Note: that the step expects executables to be already prepared by previous steps

Building NPM packages (with single executables; platform-specific):

``` sh
scripts/build_npm 1.32.2
```
Note: that the step expects executables to be already prepared by previous steps

### Building packages (alternative for Linux)

``` sh
docker buildx create --use --driver=docker-container --name container --bootstrap
docker buildx build . --cache-to type=local,dest=.cache,mode=max --cache-from type=local,src=.cache --platform=linux/amd64 --builder=container --progress plain -o dist -f Dockerfile.build
```

Note: command will produce binaries and .whl for selected platform in dist folder

For `musl` binaries, use `Dockerfile.build-musl`

### Building the Docker

``` sh
docker build -t icloudpd_dev_ .
```
Note: command packs existing musl binaries from dist folder

### Developing Documentation

To compile docs and start web server with hot reloading:

``` sh
sphinx-autobuild docs docs/_build/html
```

## How to write a unit test

The unit tests are a very important asset of this project. Due to our 100% test coverage we can safely use great tools like [Dependabot](dependabot.com) and be sure that the implementation of a new feature or fixing of a bug does not lead to further issues.

We're relying on [pytest](pytest.org) for the creation of our tests and [VCR.py](https://github.com/kevin1024/vcrpy) for automatic mocking of the communication to iCloud. This makes the creation of test cases rather simple as you don't have to deal with the communication to iCloud itself and can just focus on the "real test". Both tools maintain great howtos that can be found here:

* pytest documentation: https://docs.pytest.org/en/stable/
* VCR.py documentation: https://vcrpy.readthedocs.io/en/latest/

It is highly recommended having a look at those.

The process is mostly like this (assuming we're talking about a bug fix here...)

1. Is there already a related test case existing? If so you can just check if an existing test needs to check for another situation.
1. If not, then you need to make sure you have corresponding test-data at hand; that means: your iCloud photos library should have a constellation that leads to the error in `icloudpd`.
1. Add a test-function that runs `icloudpd` with the necessary start parameters, referencing to a new cassette file.
1. **VERY IMPORTANT:** the real iCloud response is cached, so every image is saved in the cassette. That means:
   1. Don't use private photos!
   1. Keep the dataset small (p.e. using `--recent`)
   1. Remove your personal information from the cached REST-response (Name, email addresses)
1. Go back to the previous step and verify again that you followed the recommendations!
1. Now you can start adding tests.

Refer to the existing tests for inspiration. A very simple test to understand the basic idea might be the test for the listing of albums option in `tests/test_listing_albums.py`.

When testing a bug fix it is important to test the faulty behavior and also the expected behavior.

## How to release

We have GitHub actions taking care for building, testing, and releasing software. Building and testing are happening automatically on git pushed, pull requests, and merges. For releases the following steps are manual:
- Bump version in all files, including all source files
- Update CHANGELOG.md with release changes if they were not added with commits
- Update CHANGELOG.md with date of the release
- Commit & push changes (either directly or through pull requests)
- Add version tag to head (`git tag v1.32.2`) and push it to master (`git push origin v1.32.2`) -- there seems to be no way to do that in UI


================================================
FILE: Dockerfile
================================================
FROM alpine:3.18 AS runtime_amd64_none
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"
RUN apk update && apk add --no-cache tzdata musl-locales musl-locales-lang
WORKDIR /app
COPY dist/icloud-*.*.*-linux-musl-amd64 icloud
COPY dist/icloudpd-*.*.*-linux-musl-amd64 icloudpd

FROM alpine:3.18 AS runtime_arm64_none
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"
RUN apk update && apk add --no-cache tzdata musl-locales musl-locales-lang
WORKDIR /app
COPY dist/icloud-*.*.*-linux-musl-arm64 icloud
COPY dist/icloudpd-*.*.*-linux-musl-arm64 icloudpd

FROM alpine:3.18 AS runtime_arm_v7
ENV MUSL_LOCPATH="/usr/share/i18n/locales/musl"
RUN apk update && apk add --no-cache tzdata musl-locales musl-locales-lang
WORKDIR /app
COPY dist/icloud-*.*.*-linux-musl-arm32v7 icloud
COPY dist/icloudpd-*.*.*-linux-musl-arm32v7 icloudpd

FROM runtime_${TARGETARCH}_${TARGETVARIANT:-none} AS runtime
ENV TZ=UTC
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

RUN chmod +x /app/entrypoint.sh

# Default entrypoint allows command selection
ENTRYPOINT ["/app/entrypoint.sh"]


================================================
FILE: Dockerfile.build
================================================
# Multi-arch build (local):
#     docker buildx create --use --driver=docker-container --name container --bootstrap
#     docker buildx build . --cache-to type=local,dest=.cache,mode=max --cache-from type=local,src=.cache --platform=linux/arm64 --builder=container --progress plain -o dist -f Dockerfile.build
# ,linux/amd64,linux/arm/v7
# rust links from https://forge.rust-lang.org/infra/other-installation-methods.html#standalone-installers
# arm7l instead of v6 issue: https://stackoverflow.com/questions/78535054/how-do-you-docker-buildx-build-for-arm-v6-on-qemu-emulated-platforms-that-p
Download .txt
gitextract_lkob7b_6/

├── .devcontainer/
│   ├── node/
│   │   └── devcontainer.json
│   └── python/
│       └── devcontainer.json
├── .dockerignore
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug-report.md
│   │   ├── feature-request.md
│   │   └── generic.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── build-docs.yml
│       ├── build-package.yml
│       ├── codeql-analysis.yml
│       ├── compile-notes.yml
│       ├── create-release.yml
│       ├── extract-changelog.yml
│       ├── patch-version.yml
│       ├── produce-artifacts.yml
│       ├── publish-docs.yml
│       ├── publish.yml
│       ├── quality-checks.yml
│       └── refresh-docs.yml
├── .gitignore
├── .vscode/
│   └── settings.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.build
├── Dockerfile.build-doc
├── Dockerfile.build-musl
├── EXPERIMENTAL.md
├── LICENSE.md
├── README.md
├── README_AUR.md
├── README_DOCKER.md
├── README_NPM.md
├── README_PYPI.md
├── binary_dist/
│   ├── pyproject.toml
│   └── src/
│       ├── icloud/
│       │   ├── __init__.py
│       │   └── __main__.py
│       └── icloudpd/
│           ├── __init__.py
│           └── __main__.py
├── docs/
│   ├── authentication.md
│   ├── conf.py
│   ├── index.md
│   ├── install.md
│   ├── mode.md
│   ├── naming.md
│   ├── nas.md
│   ├── raw.md
│   ├── reference.md
│   ├── size.md
│   └── webui.md
├── examples/
│   └── cron_script.sh.example
├── help_current.txt
├── jq/
│   └── version.jq
├── npm/
│   ├── @icloudpd/
│   │   ├── darwin-arm64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── darwin-x64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-arm/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-arm64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   ├── linux-x64/
│   │   │   ├── README.md
│   │   │   └── package.json
│   │   └── win32-x64/
│   │       ├── README.md
│   │       └── package.json
│   └── icloudpd/
│       ├── bin/
│       │   └── icloudpd.js
│       ├── package.json
│       └── preinstall.js
├── pyproject.toml
├── requirements-pip.txt
├── scripts/
│   ├── build
│   ├── build_bin1
│   ├── build_binary_dist_macos
│   ├── build_binary_dist_windows
│   ├── build_npm
│   ├── build_static
│   ├── build_whl
│   ├── clean
│   ├── clone_whl_version
│   ├── compile_compatibility.py
│   ├── compile_matrix.py
│   ├── compile_tzlc.py
│   ├── extract_releasenotes
│   ├── format
│   ├── get_version
│   ├── install_deps
│   ├── lint
│   ├── npx_optional
│   ├── npx_optional_touch
│   ├── patch_version
│   ├── publish_npm
│   ├── publish_pypi
│   ├── run_all_checks
│   ├── test
│   ├── test_python_versions.sh
│   ├── type_check
│   └── unpublish_npm
├── src/
│   ├── foundation/
│   │   ├── __init__.py
│   │   ├── core/
│   │   │   ├── __init__.py
│   │   │   └── optional/
│   │   │       └── __init__.py
│   │   ├── http.py
│   │   ├── json.py
│   │   ├── predicates.py
│   │   ├── py.typed
│   │   ├── string.py
│   │   └── string_utils.py
│   ├── icloudpd/
│   │   ├── __init__.py
│   │   ├── authentication.py
│   │   ├── autodelete.py
│   │   ├── base.py
│   │   ├── cli.py
│   │   ├── config.py
│   │   ├── constants.py
│   │   ├── counter.py
│   │   ├── download.py
│   │   ├── email_notifications.py
│   │   ├── exif_datetime.py
│   │   ├── filename_policies.py
│   │   ├── log_level.py
│   │   ├── logger.py
│   │   ├── mfa_provider.py
│   │   ├── password_provider.py
│   │   ├── paths.py
│   │   ├── progress.py
│   │   ├── py.typed
│   │   ├── server/
│   │   │   ├── __init__.py
│   │   │   ├── static/
│   │   │   │   ├── js/
│   │   │   │   │   └── toast.js
│   │   │   │   ├── manifest.json
│   │   │   │   └── style/
│   │   │   │       └── style.css
│   │   │   └── templates/
│   │   │       ├── auth_error.html
│   │   │       ├── code.html
│   │   │       ├── code_submitted.html
│   │   │       ├── index.html
│   │   │       ├── no_input.html
│   │   │       ├── password.html
│   │   │       ├── password_submitted.html
│   │   │       └── status.html
│   │   ├── status.py
│   │   ├── string_helpers.py
│   │   └── xmp_sidecar.py
│   ├── pyicloud_ipd/
│   │   ├── __init__.py
│   │   ├── asset_version.py
│   │   ├── base.py
│   │   ├── cmdline.py
│   │   ├── exceptions.py
│   │   ├── file_match.py
│   │   ├── item_type.py
│   │   ├── live_photo_mov_filename_policy.py
│   │   ├── py.typed
│   │   ├── raw_policy.py
│   │   ├── services/
│   │   │   ├── __init__.py
│   │   │   └── photos.py
│   │   ├── session.py
│   │   ├── sms.py
│   │   ├── utils.py
│   │   └── version_size.py
│   └── starters/
│       ├── __init__.py
│       ├── icloud.py
│       └── icloudpd.py
└── tests/
    ├── __init__.py
    ├── cookie/
    │   ├── jdoegmailcom
    │   └── jdoegmailcom.session
    ├── data/
    │   └── parse_trusted_phone_numbers_payload_valid.html
    ├── helpers/
    │   └── __init__.py
    ├── test_authentication.py
    ├── test_autodelete_photos.py
    ├── test_cli.py
    ├── test_download_live_photos.py
    ├── test_download_live_photos_id.py
    ├── test_download_photos.py
    ├── test_download_photos_id.py
    ├── test_download_videos.py
    ├── test_email_notifications.py
    ├── test_filenames.py
    ├── test_folder_structure.py
    ├── test_http.py
    ├── test_issue_1220_only_print_filenames_dedup_bug.py
    ├── test_json_rules.py
    ├── test_keep_icloud_mode.py
    ├── test_listing_albums.py
    ├── test_listing_libraries.py
    ├── test_listing_recent_photos.py
    ├── test_logger.py
    ├── test_session_connection_errors.py
    ├── test_string_helpers.py
    ├── test_two_step_auth.py
    ├── test_xmp_sidecar.py
    └── vcr_cassettes/
        ├── 2fa_flow_invalid_code.yml
        ├── 2fa_flow_valid_code.yml
        ├── 2fa_flow_valid_code_zero_lead.yml
        ├── 2sa_flow_invalid_code.yml
        ├── 2sa_flow_valid_code.yml
        ├── auth_non_2fa.yml
        ├── auth_requires_2fa.yml
        ├── autodelete_photos.yml
        ├── autodelete_photos_heic.yml
        ├── download_autodelete_photos_part1.yml
        ├── download_autodelete_photos_part2.yml
        ├── download_live_photos.yml
        ├── failed_auth.yml
        ├── failed_auth_503.yml
        ├── fallback_raw_password.yml
        ├── listing_albums.yml
        ├── listing_photos.yml
        ├── listing_photos_bad_filename.yml
        ├── listing_photos_bad_filename_base64_encoding.yml
        ├── listing_photos_bad_filename_utf8_encoding.yml
        ├── listing_photos_chinese.yml
        ├── listing_photos_filename_string_encoding.yml
        ├── listing_photos_keep_icloud_recent_days.yml
        ├── listing_photos_missing_downloadUrl.yml
        ├── listing_photos_missing_filenameEnc.yml
        ├── listing_photos_missing_item_type.yml
        ├── listing_photos_missing_item_type_value.yml
        ├── listing_photos_no_delete.yml
        ├── listing_photos_raw.yml
        ├── listing_photos_raw_alt.yml
        ├── listing_photos_raw_alt_adj.yml
        ├── listing_photos_resume.yml
        ├── listing_photos_two_sizes.yml
        ├── listing_videos.yml
        └── successful_auth.yml
Download .txt
SYMBOL INDEX (674 symbols across 68 files)

FILE: scripts/compile_compatibility.py
  function content_checker (line 10) | def content_checker(filepath):

FILE: scripts/compile_matrix.py
  function _stats (line 7) | def _stats(files):
  function _matrix (line 17) | def _matrix(files, special):
  function print_breakdowns (line 45) | def print_breakdowns(folder, special_content_checker, special_pair):

FILE: scripts/compile_tzlc.py
  function special_content_checker (line 9) | def special_content_checker(expected_content):

FILE: src/foundation/__init__.py
  class VersionInfo (line 22) | class VersionInfo(NamedTuple):
  function version_info_formatted (line 36) | def version_info_formatted() -> str:
  function bytes_decode (line 46) | def bytes_decode(encoding: str) -> Callable[[bytes], str]:
  function wrap_param_in_exception (line 57) | def wrap_param_in_exception(caption: str, func: Callable[[T_in], T_out])...
  function flat_dict (line 90) | def flat_dict(input: Iterable[Mapping[T_in, T_out]]) -> Mapping[T_in, T_...
  function split_with_alternatives (line 101) | def split_with_alternatives(splitter: Container[_T], inp: Iterable[_T]) ...
  function two_tuple (line 118) | def two_tuple(k: _T, v: _T2) -> Tuple[_T, _T2]:
  function unique_sequence (line 126) | def unique_sequence(inp: Iterable[_T]) -> Sequence[_T]:

FILE: src/foundation/core/__init__.py
  function compose (line 13) | def compose(
  function identity (line 28) | def identity(value: _T_inv) -> _T_inv:
  function constant (line 35) | def constant(value: _T_inv) -> Callable[[_T_contra], _T_inv]:
  function pipe (line 46) | def pipe(
  function apply_reverse (line 55) | def apply_reverse(input: _T_contra) -> Callable[[Callable[[_T_contra], _...
  function curry2 (line 77) | def curry2(
  function uncurry2 (line 101) | def uncurry2(
  function curry3 (line 125) | def curry3(
  function fst (line 140) | def fst(t: Tuple[_T_inv, _T2_contra]) -> _T_inv:
  function snd (line 148) | def snd(t: Tuple[_T_contra, _T_inv]) -> _T_inv:
  function flip (line 156) | def flip(
  function compact2 (line 174) | def compact2(
  function expand2 (line 185) | def expand2(
  function pipe2 (line 194) | def pipe2(
  function arrow (line 209) | def arrow(
  function partial_1_1 (line 219) | def partial_1_1(
  function partial_2_1 (line 234) | def partial_2_1(
  function filter_ (line 249) | def filter_(f: Callable[[_T_contra], bool], p1: Iterable[_T_contra]) -> ...
  function filter_guarded (line 259) | def filter_guarded(
  function map_ (line 271) | def map_(f: Callable[[_T_contra], _T_co], p1: Iterable[_T_contra]) -> It...
  function tee_ (line 281) | def tee_(inp: Iterable[_T_contra]) -> Tuple[Iterable[_T_contra], Iterabl...
  function zip_longest_ (line 295) | def zip_longest_(
  function unzip (line 307) | def unzip(
  function chain_from_iterable (line 325) | def chain_from_iterable(inp: Iterable[Iterable[_T_inv]]) -> Iterable[_T_...
  function skip (line 329) | def skip(inp: int, p1: Iterable[_T_co]) -> Iterable[_T_co]:
  function take (line 340) | def take(inp: int, p1: Iterable[_T_co]) -> Iterable[_T_co]:

FILE: src/foundation/core/optional/__init__.py
  function bind (line 9) | def bind(
  function lift2 (line 52) | def lift2(
  function lift3 (line 86) | def lift3(
  function fromMaybe (line 102) | def fromMaybe(default: _Tin) -> Callable[[_Tin | None], _Tin]:

FILE: src/foundation/http.py
  function cookie_to_pair (line 14) | def cookie_to_pair(cookie: Cookie) -> Tuple[str, str | None]:
  function cookie_str_to_dict (line 26) | def cookie_str_to_dict(cookie_header: str) -> Mapping[str, str]:
  function is_streaming_response (line 34) | def is_streaming_response(response: Response) -> bool:
  function response_body (line 42) | def response_body(response: Response) -> Any:
  function request_body (line 52) | def request_body(request: PreparedRequest) -> Any:
  function response_to_har_entry (line 61) | def response_to_har_entry(response: Response) -> Mapping[str, Any]:

FILE: src/foundation/json.py
  function extract_context (line 18) | def extract_context(context: Context, pair: Tuple[str, T1]) -> Tuple[Con...
  function first (line 29) | def first(input: Iterable[T1]) -> T1 | StopIteration:
  function first_or_default (line 35) | def first_or_default(input: Iterable[T1], default: T2) -> T1 | T2:
  function first_matching_rule (line 48) | def first_matching_rule(context: Context, rules: Sequence[Rule]) -> Rule...
  function _apply_rules_internal (line 59) | def _apply_rules_internal(input: Any, context: Context, rules: Sequence[...
  function _ (line 65) | def _(input: str, context: Context, rules: Sequence[Rule]) -> str | None:
  function _ (line 76) | def _(input: Tuple[str, Any], context: Context, rules: Sequence[Rule]) -...
  function apply_rules (line 91) | def apply_rules(context: Context, rules: Sequence[Rule], input: Any) -> ...
  function _ (line 96) | def _(input: Sequence[Any], context: Context, rules: Sequence[Rule]) -> ...
  function _ (line 116) | def _(input: Mapping[str, Any], context: Context, rules: Sequence[Rule])...
  function re_compile_flag (line 135) | def re_compile_flag(flags: re.RegexFlag, input: str) -> re.Pattern[str]:

FILE: src/foundation/predicates.py
  function and_ (line 8) | def and_(f1: Callable[[T], bool], f2: Callable[[T], bool]) -> Callable[[...
  function or_ (line 24) | def or_(f1: Callable[[T], bool], f2: Callable[[T], bool]) -> Callable[[T...
  function not_ (line 40) | def not_(f: Callable[[T], bool]) -> Callable[[T], bool]:
  function xor_ (line 53) | def xor_(f1: Callable[[T], bool], f2: Callable[[T], bool]) -> Callable[[...
  function always_true (line 69) | def always_true(_: T) -> bool:
  function always_false (line 80) | def always_false(_: T) -> bool:
  function eq_pred (line 91) | def eq_pred(expected: T) -> Callable[[T], bool]:
  function ne_pred (line 103) | def ne_pred(expected: T) -> Callable[[T], bool]:
  function in_pred (line 115) | def in_pred(container: list[T] | set[T] | tuple[T, ...]) -> Callable[[T]...
  function not_in_pred (line 127) | def not_in_pred(container: list[T] | set[T] | tuple[T, ...]) -> Callable...

FILE: src/foundation/string.py
  function obfuscate (line 7) | def obfuscate(_input: Any) -> str:
  function _ (line 12) | def _(input: str) -> str:

FILE: src/foundation/string_utils.py
  function strip (line 8) | def strip(s: str) -> str:
  function lower (line 17) | def lower(s: str) -> str:
  function upper (line 26) | def upper(s: str) -> str:
  function endswith (line 35) | def endswith(suffix: str | tuple[str, ...]) -> Callable[[str], bool]:
  function startswith (line 50) | def startswith(prefix: str) -> Callable[[str], bool]:
  function contains (line 62) | def contains(substring: str) -> Callable[[str], bool]:
  function eq (line 74) | def eq(expected: str) -> Callable[[str], bool]:
  function replace (line 86) | def replace(old: str, new: str) -> Callable[[str], str]:
  function split (line 96) | def split(separator: str) -> Callable[[str], list[str]]:
  function join (line 106) | def join(separator: str) -> Callable[[list[str]], str]:
  function is_empty (line 116) | def is_empty(s: str) -> bool:
  function is_not_empty (line 127) | def is_not_empty(s: str) -> bool:
  function replace_extension (line 147) | def replace_extension(new_ext: str) -> Callable[[str], str]:

FILE: src/icloudpd/authentication.py
  function prompt_int_range (line 15) | def prompt_int_range(message: str, default: str, min_val: int, max_val: ...
  function prompt_string (line 31) | def prompt_string(message: str) -> str:
  function is_empty_string (line 37) | def is_empty_string(input: str) -> bool:
  function is_valid_device_index (line 42) | def is_valid_device_index(input: str, device_count: int, alphabet: str) ...
  function is_valid_six_digit_code (line 51) | def is_valid_six_digit_code(input: str) -> bool:
  function echo (line 56) | def echo(message: str) -> None:
  function authenticator (line 61) | def authenticator(
  function request_2sa (line 120) | def request_2sa(icloud: PyiCloudService, logger: logging.Logger) -> None:
  function request_2fa (line 152) | def request_2fa(icloud: PyiCloudService, logger: logging.Logger) -> None:
  function request_2fa_web (line 240) | def request_2fa_web(

FILE: src/icloudpd/autodelete.py
  function delete_file (line 20) | def delete_file(logger: logging.Logger, path: str) -> bool:
  function delete_file_dry_run (line 27) | def delete_file_dry_run(logger: logging.Logger, path: str) -> bool:
  function autodelete_photos (line 33) | def autodelete_photos(

FILE: src/icloudpd/base.py
  function build_filename_cleaner (line 86) | def build_filename_cleaner(keep_unicode: bool) -> Callable[[str], str]:
  function lp_filename_concatinator (line 104) | def lp_filename_concatinator(filename: str) -> str:
  function lp_filename_original (line 119) | def lp_filename_original(filename: str) -> str:
  function ask_password_in_console (line 127) | def ask_password_in_console(_user: str) -> str | None:
  function get_password_from_webui (line 131) | def get_password_from_webui(
  function update_password_status_in_webui (line 159) | def update_password_status_in_webui(status_exchange: StatusExchange, _u:...
  function update_auth_error_in_webui (line 163) | def update_auth_error_in_webui(status_exchange: StatusExchange, error: s...
  function dummy_password_writter (line 174) | def dummy_password_writter(_u: str, _p: str) -> None:
  function keyring_password_writter (line 178) | def keyring_password_writter(logger: Logger) -> Callable[[str, str], None]:
  function skip_created_generator (line 188) | def skip_created_generator(
  function ensure_tzinfo (line 202) | def ensure_tzinfo(tz: datetime.tzinfo, input: datetime.datetime) -> date...
  function create_logger (line 211) | def create_logger(config: GlobalConfig) -> logging.Logger:
  function run_with_configs (line 236) | def run_with_configs(global_config: GlobalConfig, user_configs: Sequence...
  function _process_all_users_once (line 312) | def _process_all_users_once(
  function notificator_builder (line 463) | def notificator_builder(
  function offset_to_datetime (line 501) | def offset_to_datetime(offset: Any) -> datetime.datetime:
  function _ (line 506) | def _(offset: datetime.datetime) -> datetime.datetime:
  function _ (line 511) | def _(offset: datetime.timedelta) -> datetime.datetime:
  function where_builder (line 515) | def where_builder(
  function skip_created_before_message (line 546) | def skip_created_before_message(
  function skip_created_after_message (line 555) | def skip_created_after_message(
  function download_builder (line 564) | def download_builder(
  function delete_photo (line 808) | def delete_photo(
  function delete_photo_dry_run (line 843) | def delete_photo_dry_run(
  function dump_responses (line 858) | def dump_responses(dumper: Callable[[Any], None], responses: List[Mappin...
  function asset_type_skip_message (line 865) | def asset_type_skip_message(
  function core_single_run (line 875) | def core_single_run(

FILE: src/icloudpd/cli.py
  function map_align_raw_to_enum (line 27) | def map_align_raw_to_enum(align_raw_str: str) -> RawTreatmentPolicy:
  function add_options_for_user (line 37) | def add_options_for_user(parser: argparse.ArgumentParser) -> argparse.Ar...
  function add_user_option (line 262) | def add_user_option(parser: argparse.ArgumentParser) -> argparse.Argumen...
  function parse_mfa_provider (line 280) | def parse_mfa_provider(provider: str) -> MFAProvider:
  function add_global_options (line 293) | def add_global_options(parser: argparse.ArgumentParser) -> argparse.Argu...
  function log_level (line 364) | def log_level(inp: str) -> LogLevel:
  function parse_timestamp_or_timedelta_tz_error (line 375) | def parse_timestamp_or_timedelta_tz_error(
  function format_help_for_parser_ (line 389) | def format_help_for_parser_(parser: argparse.ArgumentParser) -> str:
  function format_help (line 393) | def format_help() -> str:
  function map_to_config (line 431) | def map_to_config(user_ns: argparse.Namespace) -> UserConfig:
  function parse (line 478) | def parse(args: Sequence[str]) -> Tuple[GlobalConfig, Sequence[UserConfi...
  function cli (line 536) | def cli() -> int:
  function validate_folder_structure (line 612) | def validate_folder_structure(folder_structure: str) -> str:

FILE: src/icloudpd/config.py
  class _DefaultConfig (line 16) | class _DefaultConfig:
  class UserConfig (line 56) | class UserConfig(_DefaultConfig):
  class GlobalConfig (line 62) | class GlobalConfig:

FILE: src/icloudpd/counter.py
  class Counter (line 6) | class Counter:
    method __init__ (line 7) | def __init__(self, value: int = 0):
    method increment (line 12) | def increment(self) -> None:
    method reset (line 16) | def reset(self) -> None:
    method value (line 20) | def value(self) -> int:

FILE: src/icloudpd/download.py
  function update_mtime (line 23) | def update_mtime(created: datetime.datetime, download_path: str) -> None:
  function set_utime (line 37) | def set_utime(download_path: str, created_date: datetime.datetime) -> None:
  function mkdirs_for_path (line 46) | def mkdirs_for_path(logger: logging.Logger, download_path: str) -> bool:
  function mkdirs_for_path_dry_run (line 62) | def mkdirs_for_path_dry_run(logger: logging.Logger, download_path: str) ...
  function download_response_to_path (line 73) | def download_response_to_path(
  function download_response_to_path_dry_run (line 90) | def download_response_to_path_dry_run(
  function download_media (line 106) | def download_media(

FILE: src/icloudpd/email_notifications.py
  function send_2sa_notification (line 9) | def send_2sa_notification(

FILE: src/icloudpd/exif_datetime.py
  function get_photo_exif (line 10) | def get_photo_exif(logger: logging.Logger, path: str) -> str | None:
  function set_photo_exif (line 20) | def set_photo_exif(logger: logging.Logger, path: str, date: str) -> None:

FILE: src/icloudpd/filename_policies.py
  function build_filename_with_policies (line 15) | def build_filename_with_policies(
  function create_filename_builder (line 29) | def create_filename_builder(

FILE: src/icloudpd/log_level.py
  class LogLevel (line 4) | class LogLevel(Enum):
    method __str__ (line 9) | def __str__(self) -> str:

FILE: src/icloudpd/logger.py
  class IPDLogger (line 9) | class IPDLogger(logging.Logger):
    method __init__ (line 12) | def __init__(self, name: str, level: int = INFO):
    method set_tqdm (line 17) | def set_tqdm(self, tdqm: Any) -> None:
    method set_tqdm_description (line 21) | def set_tqdm_description(self, desc: str, loglevel: int = INFO) -> None:
    method tqdm_write (line 28) | def tqdm_write(self, message: str, loglevel: int = INFO) -> None:
  function setup_logger (line 36) | def setup_logger() -> logging.Logger:

FILE: src/icloudpd/mfa_provider.py
  class MFAProvider (line 4) | class MFAProvider(Enum):
    method __str__ (line 8) | def __str__(self) -> str:

FILE: src/icloudpd/password_provider.py
  class PasswordProvider (line 4) | class PasswordProvider(Enum):
    method __str__ (line 10) | def __str__(self) -> str:

FILE: src/icloudpd/paths.py
  function remove_unicode_chars (line 6) | def remove_unicode_chars(value: str) -> str:
  function clean_filename (line 12) | def clean_filename(filename: str) -> str:
  function local_download_path (line 23) | def local_download_path(filename: str, download_dir: str) -> str:

FILE: src/icloudpd/progress.py
  class Progress (line 4) | class Progress:
    method __init__ (line 5) | def __init__(self) -> None:
    method waiting (line 16) | def waiting(self) -> int:
    method waiting (line 20) | def waiting(self, waiting: int) -> None:
    method photos_count (line 25) | def photos_count(self) -> int:
    method photos_count (line 29) | def photos_count(self, photos_count: int) -> None:
    method photos_counter (line 37) | def photos_counter(self) -> int:
    method photos_counter (line 41) | def photos_counter(self, photos_counter: int) -> None:
    method reset (line 48) | def reset(self) -> None:

FILE: src/icloudpd/server/__init__.py
  function serve_app (line 11) | def serve_app(logger: Logger, _status_exchange: StatusExchange) -> None:

FILE: src/icloudpd/status.py
  class Status (line 9) | class Status(Enum):
    method __str__ (line 18) | def __str__(self) -> str:
  class StatusExchange (line 22) | class StatusExchange:
    method __init__ (line 23) | def __init__(self) -> None:
    method get_status (line 33) | def get_status(self) -> Status:
    method replace_status (line 37) | def replace_status(self, expected_status: Status, new_status: Status) ...
    method set_payload (line 45) | def set_payload(self, payload: str) -> bool:
    method get_payload (line 57) | def get_payload(self) -> str | None:
    method set_error (line 69) | def set_error(self, error: str) -> bool:
    method get_error (line 82) | def get_error(self) -> str | None:
    method get_progress (line 93) | def get_progress(self) -> Progress:
    method set_global_config (line 97) | def set_global_config(self, global_config: GlobalConfig) -> None:
    method get_global_config (line 101) | def get_global_config(self) -> GlobalConfig | None:
    method set_user_configs (line 105) | def set_user_configs(self, user_configs: Sequence[UserConfig]) -> None:
    method get_user_configs (line 109) | def get_user_configs(self) -> Sequence[UserConfig]:
    method set_current_user (line 113) | def set_current_user(self, username: str) -> None:
    method get_current_user (line 117) | def get_current_user(self) -> str | None:
    method clear_current_user (line 121) | def clear_current_user(self) -> None:

FILE: src/icloudpd/string_helpers.py
  function truncate_middle (line 8) | def truncate_middle(string: str, length: int) -> str:
  function parse_timedelta (line 22) | def parse_timedelta(
  function parse_timestamp (line 37) | def parse_timestamp(
  function parse_timestamp_or_timedelta (line 51) | def parse_timestamp_or_timedelta(
  function splitlines (line 62) | def splitlines(inp: str) -> Sequence[str]:

FILE: src/icloudpd/xmp_sidecar.py
  class XMPMetadata (line 20) | class XMPMetadata(NamedTuple):
  function generate_xmp_file (line 37) | def generate_xmp_file(
  function build_metadata (line 79) | def build_metadata(asset_record: dict[str, Any]) -> XMPMetadata:
  function generate_xml (line 193) | def generate_xml(metadata: XMPMetadata) -> ElementTree.Element:

FILE: src/pyicloud_ipd/asset_version.py
  function add_suffix_to_filename (line 34) | def add_suffix_to_filename(suffix: str, filename: str) -> str:
  function calculate_version_filename (line 40) | def calculate_version_filename(
  class AssetVersion (line 76) | class AssetVersion:
    method __init__ (line 77) | def __init__(self, size: int, url: str, type: str, checksum: str) -> N...
    method __eq__ (line 83) | def __eq__(self, other: object) -> bool:
  function calculate_asset_version_filename (line 90) | def calculate_asset_version_filename(

FILE: src/pyicloud_ipd/base.py
  function origin_referer_headers (line 58) | def origin_referer_headers(input: str) -> Dict[str, str]:
  class TrustedPhoneContextProvider (line 62) | class TrustedPhoneContextProvider(NamedTuple):
  class PyiCloudService (line 67) | class PyiCloudService:
    method __init__ (line 78) | def __init__(
    method use_rules (line 283) | def use_rules(self, rules: Sequence[Rule]) -> Generator[Sequence[Rule]...
    method authenticate (line 291) | def authenticate(self, force_refresh: bool = False) -> None:
    method _authenticate_with_token (line 337) | def _authenticate_with_token(self) -> None:
    method _authenticate_srp (line 377) | def _authenticate_srp(self, password: str) -> None:
    method _authenticate_raw_password (line 519) | def _authenticate_raw_password(self, password: str) -> None:
    method _validate_token (line 557) | def _validate_token(self) -> Dict[str, Any]:
    method _get_auth_headers (line 588) | def _get_auth_headers(self, overrides: Dict[str, str] | None = None) -...
    method cookiejar_path (line 616) | def cookiejar_path(self) -> str:
    method session_path (line 624) | def session_path(self) -> str:
    method requires_2sa (line 632) | def requires_2sa(self) -> bool:
    method requires_2fa (line 639) | def requires_2fa(self) -> bool:
    method is_trusted_session (line 648) | def is_trusted_session(self) -> bool:
    method trusted_devices (line 653) | def trusted_devices(self) -> Sequence[Dict[str, Any]]:
    method send_request (line 661) | def send_request(self, request: PreparedRequest) -> Response:
    method get_oauth_session (line 664) | def get_oauth_session(self) -> AuthenticatedSession:
    method get_trusted_phone_numbers (line 671) | def get_trusted_phone_numbers(self) -> Sequence[TrustedDevice]:
    method send_2fa_code_sms (line 697) | def send_2fa_code_sms(self, device_id: int) -> bool:
    method send_verification_code (line 729) | def send_verification_code(self, device: Dict[str, Any]) -> bool:
    method validate_verification_code (line 737) | def validate_verification_code(self, device: Dict[str, Any], code: str...
    method validate_2fa_code_sms (line 761) | def validate_2fa_code_sms(self, device_id: int, code: str) -> bool:
    method validate_2fa_code (line 794) | def validate_2fa_code(self, code: str) -> bool:
    method trust_session (line 831) | def trust_session(self) -> bool:
    method _get_webservice_url (line 859) | def _get_webservice_url(self, ws_key: str) -> str:
    method photos (line 866) | def photos(self) -> PhotosService:
    method __unicode__ (line 873) | def __unicode__(self) -> str:
    method __str__ (line 876) | def __str__(self) -> str:
    method __repr__ (line 880) | def __repr__(self) -> str:

FILE: src/pyicloud_ipd/cmdline.py
  function main (line 19) | def main(args: Sequence[str] | None = None) -> NoReturn:

FILE: src/pyicloud_ipd/exceptions.py
  class PyiCloudException (line 1) | class PyiCloudException(Exception):
  class PyiCloudAPIResponseException (line 8) | class PyiCloudAPIResponseException(PyiCloudException):
    method __init__ (line 11) | def __init__(self, reason: str, code: str | None = None):
  class PyiCloudServiceNotActivatedException (line 21) | class PyiCloudServiceNotActivatedException(PyiCloudAPIResponseException):
  class PyiCloudServiceUnavailableException (line 27) | class PyiCloudServiceUnavailableException(PyiCloudException):
  class PyiCloudConnectionErrorException (line 33) | class PyiCloudConnectionErrorException(PyiCloudException):
  class PyiCloudFailedLoginException (line 40) | class PyiCloudFailedLoginException(PyiCloudException):
  class PyiCloudFailedMFAException (line 46) | class PyiCloudFailedMFAException(PyiCloudException):
  class PyiCloud2SARequiredException (line 52) | class PyiCloud2SARequiredException(PyiCloudException):
    method __init__ (line 55) | def __init__(self, apple_id: str):
  class PyiCloudNoStoredPasswordAvailableException (line 60) | class PyiCloudNoStoredPasswordAvailableException(PyiCloudException):
  class PyiCloudNoDevicesException (line 67) | class PyiCloudNoDevicesException(PyiCloudException):
  class PyiCloudConnectionException (line 74) | class PyiCloudConnectionException(PyiCloudException):
  class PyiCloudAPIResponseError (line 78) | class PyiCloudAPIResponseError(PyiCloudException):
    method __init__ (line 79) | def __init__(self, reason: str, code: (int | None)):
  class PyiCloud2SARequiredError (line 89) | class PyiCloud2SARequiredError(PyiCloudException):
    method __init__ (line 90) | def __init__(self, url: str):
  class NoStoredPasswordAvailable (line 95) | class NoStoredPasswordAvailable(PyiCloudException):
  class PyiCloudServiceNotActivatedErrror (line 99) | class PyiCloudServiceNotActivatedErrror(PyiCloudAPIResponseError):

FILE: src/pyicloud_ipd/file_match.py
  class FileMatchPolicy (line 4) | class FileMatchPolicy(Enum):
    method __str__ (line 8) | def __str__(self) -> str:

FILE: src/pyicloud_ipd/item_type.py
  class AssetItemType (line 4) | class AssetItemType(Enum):
    method __str__ (line 8) | def __str__(self) -> str:

FILE: src/pyicloud_ipd/live_photo_mov_filename_policy.py
  class LivePhotoMovFilenamePolicy (line 4) | class LivePhotoMovFilenamePolicy(Enum):
    method __str__ (line 8) | def __str__(self) -> str:

FILE: src/pyicloud_ipd/raw_policy.py
  class RawTreatmentPolicy (line 4) | class RawTreatmentPolicy(Enum):
    method __str__ (line 9) | def __str__(self) -> str:

FILE: src/pyicloud_ipd/services/photos.py
  function apply_file_match_policy (line 35) | def apply_file_match_policy(
  function apply_filename_cleaner (line 62) | def apply_filename_cleaner(filename_cleaner: Callable[[str], str]) -> Ca...
  function generate_fingerprint_filename (line 80) | def generate_fingerprint_filename(asset_id: str, item_type_extension: st...
  function filename_with_fallback (line 96) | def filename_with_fallback(asset_id: str, item_type_extension: str) -> C...
  function download_asset (line 115) | def download_asset(session: Session, url: str, start: int = 0) -> Response:
  function apply_raw_policy (line 131) | def apply_raw_policy(
  class PhotoLibrary (line 166) | class PhotoLibrary:
    method __init__ (line 273) | def __init__(
    method albums (line 304) | def albums(self) -> Dict[str, "PhotoAlbum"]:
    method _fetch_folders (line 351) | def _fetch_folders(self) -> Sequence[Dict[str, Any]]:
    method all (line 368) | def all(self) -> "PhotoAlbum":
    method recently_deleted (line 381) | def recently_deleted(self) -> "PhotoAlbum":
  class PhotosService (line 394) | class PhotosService(PhotoLibrary):
    method __init__ (line 400) | def __init__(self, service_root: str, session: PyiCloudSession, params...
    method private_libraries (line 424) | def private_libraries(self) -> Dict[str, PhotoLibrary]:
    method shared_libraries (line 431) | def shared_libraries(self) -> Dict[str, PhotoLibrary]:
    method _fetch_libraries (line 437) | def _fetch_libraries(self, library_type: str) -> Dict[str, PhotoLibrary]:
    method get_service_endpoint (line 463) | def get_service_endpoint(self, library_type: str) -> str:
  class PhotoAlbum (line 467) | class PhotoAlbum:
    method __init__ (line 468) | def __init__(
    method title (line 496) | def title(self) -> str:
    method __iter__ (line 499) | def __iter__(self) -> Generator["PhotoAsset", Any, None]:
    method __len__ (line 502) | def __len__(self) -> int:
    method photos_request (line 515) | def photos_request(self) -> Response:
    method photos (line 524) | def photos(self) -> Generator["PhotoAsset", Any, None]:
    method increment_offset (line 558) | def increment_offset(self, value: int) -> None:
    method _count_query_gen (line 561) | def _count_query_gen(self, obj_type: str) -> Dict[str, Any]:
    method _list_query_gen (line 582) | def _list_query_gen(
    method __unicode__ (line 713) | def __unicode__(self) -> str:
    method __str__ (line 716) | def __str__(self) -> str:
    method __repr__ (line 720) | def __repr__(self) -> str:
  class PhotoAsset (line 724) | class PhotoAsset:
    method __init__ (line 725) | def __init__(self, master_record: Dict[str, Any], asset_record: Dict[s...
    method id (line 769) | def id(self) -> str:
    method calculate_filename (line 772) | def calculate_filename(self) -> str | None:
    method filename (line 828) | def filename(self) -> str:
    method size (line 844) | def size(self) -> int:
    method created (line 848) | def created(self) -> datetime:
    method asset_date (line 860) | def asset_date(self) -> datetime:
    method added_date (line 870) | def added_date(self) -> datetime:
    method dimensions (line 877) | def dimensions(self) -> Tuple[int, int]:
    method item_type (line 884) | def item_type(self) -> AssetItemType | None:
    method item_type_extension (line 906) | def item_type_extension(self) -> str:
    method calculate_version_filename (line 915) | def calculate_version_filename(
    method versions (line 933) | def versions(self) -> Dict[VersionSize, AssetVersion]:
    method versions_with_raw_policy (line 970) | def versions_with_raw_policy(
    method download (line 984) | def download(self, session: Session, url: str, start: int = 0) -> Resp...
    method __repr__ (line 988) | def __repr__(self) -> str:

FILE: src/pyicloud_ipd/session.py
  class PyiCloudPasswordFilter (line 32) | class PyiCloudPasswordFilter(logging.Filter):
    method __init__ (line 33) | def __init__(self, password: str):
    method filter (line 37) | def filter(self, record: logging.LogRecord) -> bool:
  class PyiCloudSession (line 46) | class PyiCloudSession(Session):
    method __init__ (line 49) | def __init__(
    method observe (line 56) | def observe(self, response: Response) -> Response:
    method request (line 62) | def request(self, method: str, url, **kwargs):  # type: ignore
    method _raise_error (line 154) | def _raise_error(self, code: str, reason: str) -> NoReturn:

FILE: src/pyicloud_ipd/sms.py
  class _SMSParser (line 6) | class _SMSParser(HTMLParser):
    method __init__ (line 7) | def __init__(self) -> None:
    method handle_starttag (line 13) | def handle_starttag(self, tag: str, attrs: List[Tuple[str, str | None]...
    method handle_endtag (line 20) | def handle_endtag(self, tag: str) -> None:
    method handle_data (line 24) | def handle_data(self, data: str) -> None:
  class TrustedDevice (line 29) | class TrustedDevice(Protocol):
    method id (line 31) | def id(self) -> int: ...
    method obfuscated_number (line 33) | def obfuscated_number(self) -> str: ...
  class _InternalTrustedDevice (line 36) | class _InternalTrustedDevice(NamedTuple):
  function _map_to_trusted_device (line 41) | def _map_to_trusted_device(device: Mapping[str, Any]) -> TrustedDevice |...
  class _Response (line 49) | class _Response(Protocol):
    method status_code (line 51) | def status_code(self) -> int: ...
    method text (line 53) | def text(self) -> str: ...
  function parse_trusted_phone_numbers_response (line 56) | def parse_trusted_phone_numbers_response(response: _Response) -> Sequenc...
  function parse_trusted_phone_numbers_payload (line 63) | def parse_trusted_phone_numbers_payload(content: str) -> Sequence[Truste...
  class AuthenticatedSession (line 77) | class AuthenticatedSession(NamedTuple):
  function _oauth_const_headers (line 83) | def _oauth_const_headers() -> Mapping[str, str]:
  function _oauth_redirect_header (line 92) | def _oauth_redirect_header(domain: str) -> Mapping[str, str]:
  function _oauth_headers (line 100) | def _oauth_headers(auth_session: AuthenticatedSession) -> Mapping[str, s...
  function _auth_url (line 110) | def _auth_url(domain: str) -> str:
  class _DomainProvider (line 118) | class _DomainProvider(Protocol):
    method domain (line 120) | def domain(self) -> str: ...
  class _OAuthSessionProvider (line 123) | class _OAuthSessionProvider(Protocol):
    method oauth_session (line 125) | def oauth_session(self) -> AuthenticatedSession: ...
  class _TrustedPhoneContextProvider (line 128) | class _TrustedPhoneContextProvider(_DomainProvider, _OAuthSessionProvide...
  class Request (line 131) | class Request(Protocol):
    method method (line 133) | def method(self) -> str: ...
    method url (line 135) | def url(self) -> str: ...
    method headers (line 137) | def headers(self) -> Mapping[str, str]: ...
    method data (line 139) | def data(self) -> str | None: ...
    method json (line 141) | def json(self) -> Mapping[str, Any] | None: ...
  class _InternalRequest (line 144) | class _InternalRequest(NamedTuple):
  function build_trusted_phone_numbers_request (line 152) | def build_trusted_phone_numbers_request(context: _TrustedPhoneContextPro...
  function build_send_sms_code_request (line 169) | def build_send_sms_code_request(context: _TrustedPhoneContextProvider, d...
  function build_verify_sms_code_request (line 190) | def build_verify_sms_code_request(

FILE: src/pyicloud_ipd/utils.py
  function password_exists_in_keyring (line 38) | def password_exists_in_keyring(username: str) -> bool:
  function get_password_from_keyring (line 45) | def get_password_from_keyring(username: str) -> str | None:
  function store_password_in_keyring (line 60) | def store_password_in_keyring(username: str, password: str) -> None:
  function delete_password_in_keyring (line 71) | def delete_password_in_keyring(username: str) -> None:
  function underscore_to_camelcase (line 78) | def underscore_to_camelcase(word: str, initial_capital: bool = False) ->...
  function size_to_suffix (line 97) | def size_to_suffix(size: VersionSize) -> str:
  function disambiguate_filenames (line 101) | def disambiguate_filenames(
  function throw_on_503 (line 201) | def throw_on_503(response: Response) -> Response:
  function handle_connection_error (line 213) | def handle_connection_error(func: F) -> F:

FILE: src/pyicloud_ipd/version_size.py
  class AssetVersionSize (line 4) | class AssetVersionSize(Enum):
    method __str__ (line 11) | def __str__(self) -> str:
  class LivePhotoVersionSize (line 15) | class LivePhotoVersionSize(Enum):
    method __str__ (line 20) | def __str__(self) -> str:

FILE: tests/helpers/__init__.py
  class TestResult (line 19) | class TestResult:
    method __init__ (line 22) | def __init__(
    method stdout_bytes (line 36) | def stdout_bytes(self) -> bytes:
    method __repr__ (line 39) | def __repr__(self) -> str:
  function print_result_exception (line 43) | def print_result_exception(result: TestResult) -> TestResult:
  function path_from_project_root (line 54) | def path_from_project_root(file_name: str) -> str:
  function recreate_path (line 59) | def recreate_path(path_name: str) -> None:
  function create_files (line 66) | def create_files(data_dir: str, files_to_create: Sequence[Tuple[str, str...
  function combine_file_lists (line 84) | def combine_file_lists(
  class AssertEquality (line 97) | class AssertEquality(Protocol):
    method __call__ (line 98) | def __call__(self, __first: _T, __second: _T, __msg: str) -> None: ...
  function assert_files (line 101) | def assert_files(
  function clean_boolean_args (line 121) | def clean_boolean_args(params: Sequence[str]) -> list[str]:
  function run_main_env (line 168) | def run_main_env(
  function run_with_cassette (line 305) | def run_with_cassette(cassette_path: str, f: Callable[[_T_contra], _T_co...
  function run_cassette (line 310) | def run_cassette(
  function run_icloudpd_test (line 324) | def run_icloudpd_test(

FILE: tests/test_authentication.py
  class AuthenticationTestCase (line 25) | class AuthenticationTestCase(TestCase):
    method inject_fixtures (line 27) | def inject_fixtures(self) -> None:
    method test_failed_auth (line 33) | def test_failed_auth(self) -> None:
    method test_fallback_raw_password (line 62) | def test_fallback_raw_password(self) -> None:
    method test_successful_token_validation (line 89) | def test_successful_token_validation(self) -> None:
    method test_password_prompt_2sa (line 114) | def test_password_prompt_2sa(self) -> None:
    method test_password_prompt_2fa (line 150) | def test_password_prompt_2fa(self) -> None:
    method test_parse_trusted_phone_numbers_payload_valid (line 189) | def test_parse_trusted_phone_numbers_payload_valid(self) -> None:
    method test_parse_trusted_phone_numbers_payload_minimal (line 198) | def test_parse_trusted_phone_numbers_payload_minimal(self) -> None:
    method test_parse_trusted_phone_numbers_payload_missing_node0 (line 205) | def test_parse_trusted_phone_numbers_payload_missing_node0(self) -> None:
    method test_parse_trusted_phone_numbers_payload_missing_node1 (line 210) | def test_parse_trusted_phone_numbers_payload_missing_node1(self) -> None:
    method test_parse_trusted_phone_numbers_payload_missing_node2 (line 215) | def test_parse_trusted_phone_numbers_payload_missing_node2(self) -> None:
    method test_parse_trusted_phone_numbers_payload_missing_node3 (line 220) | def test_parse_trusted_phone_numbers_payload_missing_node3(self) -> None:
    method test_parse_trusted_phone_numbers_payload_empty_list (line 225) | def test_parse_trusted_phone_numbers_payload_empty_list(self) -> None:
    method test_parse_trusted_phone_numbers_payload_invalid_missing_id (line 230) | def test_parse_trusted_phone_numbers_payload_invalid_missing_id(self) ...
    method test_parse_trusted_phone_numbers_payload_invalid_missing_number (line 235) | def test_parse_trusted_phone_numbers_payload_invalid_missing_number(se...
    method test_non_2fa (line 240) | def test_non_2fa(self) -> None:
    method test_failed_auth_503 (line 267) | def test_failed_auth_503(self) -> None:
    method test_failed_auth_503_watch (line 294) | def test_failed_auth_503_watch(self) -> None:
    method test_connection_error (line 329) | def test_connection_error(self) -> None:
    method test_timeout_error (line 369) | def test_timeout_error(self) -> None:
    method test_timeout (line 409) | def test_timeout(self) -> None:
  class _TrustedDevice (line 450) | class _TrustedDevice(NamedTuple):

FILE: tests/test_autodelete_photos.py
  class AutodeletePhotosTestCase (line 28) | class AutodeletePhotosTestCase(TestCase):
    method inject_fixtures (line 30) | def inject_fixtures(self) -> None:
    method test_autodelete_invalid_creation_date (line 35) | def test_autodelete_invalid_creation_date(self) -> None:
    method test_download_autodelete_photos (line 144) | def test_download_autodelete_photos(self) -> None:
    method test_autodelete_photos (line 242) | def test_autodelete_photos(self) -> None:
    method test_retry_delete_after_download_session_error (line 351) | def test_retry_delete_after_download_session_error(self) -> None:
    method test_retry_fail_delete_after_download_session_error (line 447) | def test_retry_fail_delete_after_download_session_error(self) -> None:
    method test_retry_delete_after_download_internal_error (line 540) | def test_retry_delete_after_download_internal_error(self) -> None:
    method test_retry_fail_delete_after_download_internal_error (line 622) | def test_retry_fail_delete_after_download_internal_error(self) -> None:
    method test_autodelete_photos_dry_run (line 698) | def test_autodelete_photos_dry_run(self) -> None:
    method test_autodelete_photos_folder_none (line 812) | def test_autodelete_photos_folder_none(self) -> None:
    method test_autodelete_photos_lp (line 903) | def test_autodelete_photos_lp(self) -> None:
    method test_autodelete_photos_lp_heic (line 995) | def test_autodelete_photos_lp_heic(self) -> None:

FILE: tests/test_cli.py
  class CliTestCase (line 28) | class CliTestCase(TestCase):
    method inject_fixtures (line 30) | def inject_fixtures(self, caplog: pytest.LogCaptureFixture) -> None:
    method test_cli_help (line 35) | def test_cli_help(self) -> None:
    method test_cli_parser (line 72) | def test_cli_parser(self) -> None:
    method test_cli (line 393) | def test_cli(self) -> None:
    method test_log_levels (line 397) | def test_log_levels(self) -> None:
    method test_tqdm (line 431) | def test_tqdm(self) -> None:
    method test_unicode_directory (line 453) | def test_unicode_directory(self) -> None:
    method test_missing_directory (line 476) | def test_missing_directory(self) -> None:
    method test_missing_directory_param (line 500) | def test_missing_directory_param(self) -> None:
    method test_conflict_options_delete_after_download_and_auto_delete (line 518) | def test_conflict_options_delete_after_download_and_auto_delete(self) ...
    method test_conflict_options_delete_after_download_and_keep_icloud_recent_days (line 536) | def test_conflict_options_delete_after_download_and_keep_icloud_recent...

FILE: tests/test_download_live_photos.py
  class DownloadLivePhotoTestCase (line 14) | class DownloadLivePhotoTestCase(TestCase):
    method inject_fixtures (line 16) | def inject_fixtures(self) -> None:
    method test_lp_filename_generator (line 20) | def test_lp_filename_generator(self) -> None:
    method test_skip_existing_downloads_for_live_photos (line 31) | def test_skip_existing_downloads_for_live_photos(self) -> None:
    method test_skip_existing_live_photodownloads (line 65) | def test_skip_existing_live_photodownloads(self) -> None:
    method test_skip_existing_live_photo_print_filenames (line 107) | def test_skip_existing_live_photo_print_filenames(self) -> None:

FILE: tests/test_download_live_photos_id.py
  class DownloadLivePhotoNameIDTestCase (line 13) | class DownloadLivePhotoNameIDTestCase(TestCase):
    method inject_fixtures (line 15) | def inject_fixtures(self) -> None:
    method test_skip_existing_downloads_for_live_photos_name_id7 (line 19) | def test_skip_existing_downloads_for_live_photos_name_id7(self) -> None:
    method test_skip_existing_live_photodownloads_name_id7 (line 53) | def test_skip_existing_live_photodownloads_name_id7(self) -> None:
    method test_skip_existing_live_photo_print_filenames_name_id7 (line 95) | def test_skip_existing_live_photo_print_filenames_name_id7(self) -> None:

FILE: tests/test_download_photos.py
  class DownloadPhotoTestCase (line 34) | class DownloadPhotoTestCase(TestCase):
    method inject_fixtures (line 36) | def inject_fixtures(self) -> None:
    method test_download_and_skip_existing_photos (line 40) | def test_download_and_skip_existing_photos(self) -> None:
    method test_download_photos_and_set_exif (line 117) | def test_download_photos_and_set_exif(self) -> None:
    method test_download_photos_and_get_exif_exceptions (line 194) | def test_download_photos_and_get_exif_exceptions(self) -> None:
    method test_skip_existing_downloads (line 245) | def test_skip_existing_downloads(self) -> None:
    method test_until_found (line 291) | def test_until_found(self) -> None:
    method test_handle_io_error (line 392) | def test_handle_io_error(self) -> None:
    method test_handle_session_error_during_download (line 435) | def test_handle_session_error_during_download(self) -> None:
    method test_handle_session_error_during_photo_iteration (line 496) | def test_handle_session_error_during_photo_iteration(self) -> None:
    method test_handle_albums_error (line 557) | def test_handle_albums_error(self) -> None:
    method test_missing_size (line 601) | def test_missing_size(self) -> None:
    method test_size_fallback_to_original (line 668) | def test_size_fallback_to_original(self) -> None:
    method test_adjusted_size_fallback_to_original (line 724) | def test_adjusted_size_fallback_to_original(self) -> None:
    method test_force_size (line 782) | def test_force_size(self) -> None:
    method test_download_two_sizes_with_force_size (line 834) | def test_download_two_sizes_with_force_size(self) -> None:
    method test_invalid_creation_date (line 899) | def test_invalid_creation_date(self) -> None:
    method test_creation_date_without_century (line 955) | def test_creation_date_without_century(self) -> None:
    method test_creation_date_prior_1970 (line 1011) | def test_creation_date_prior_1970(self) -> None:
    method test_missing_item_type (line 1059) | def test_missing_item_type(self) -> None:
    method test_missing_item_type_value (line 1087) | def test_missing_item_type_value(self) -> None:
    method test_download_and_dedupe_existing_photos (line 1115) | def test_download_and_dedupe_existing_photos(self) -> None:
    method test_download_photos_and_set_exif_exceptions (line 1209) | def test_download_photos_and_set_exif_exceptions(self) -> None:
    method test_download_chinese (line 1263) | def test_download_chinese(self) -> None:
    method test_download_one_recent_live_photo (line 1311) | def test_download_one_recent_live_photo(self) -> None:
    method test_download_one_recent_live_photo_chinese (line 1366) | def test_download_one_recent_live_photo_chinese(self) -> None:
    method test_download_and_delete_after (line 1423) | def test_download_and_delete_after(self) -> None:
    method test_download_and_not_delete_after_when_exists (line 1465) | def test_download_and_not_delete_after_when_exists(self) -> None:
    method test_download_and_delete_after_fail (line 1507) | def test_download_and_delete_after_fail(self) -> None:
    method test_download_over_old_original_photos (line 1543) | def test_download_over_old_original_photos(self) -> None:
    method test_download_normalized_names (line 1604) | def test_download_normalized_names(self) -> None:
    method test_handle_internal_error_during_download (line 1643) | def test_handle_internal_error_during_download(self) -> None:
    method test_handle_internal_error_during_photo_iteration (line 1692) | def test_handle_internal_error_during_photo_iteration(self) -> None:
    method test_handle_io_error_mkdir (line 1741) | def test_handle_io_error_mkdir(self) -> None:
    method test_dry_run (line 1775) | def test_dry_run(self) -> None:
    method test_download_after_delete_dry_run (line 1819) | def test_download_after_delete_dry_run(self) -> None:
    method test_download_raw_photos (line 1869) | def test_download_raw_photos(self) -> None:
    method test_download_two_sizes (line 1911) | def test_download_two_sizes(self) -> None:
    method test_download_raw_alt_photos (line 1954) | def test_download_raw_alt_photos(self) -> None:
    method test_download_raw_photos_policy_alt_with_adj (line 2004) | def test_download_raw_photos_policy_alt_with_adj(self) -> None:
    method test_download_raw_photos_policy_orig (line 2052) | def test_download_raw_photos_policy_orig(self) -> None:
    method test_download_raw_photos_policy_as_is (line 2102) | def test_download_raw_photos_policy_as_is(self) -> None:
    method test_download_bad_filename_base64_encoding (line 2152) | def test_download_bad_filename_base64_encoding(self) -> None:
    method test_download_bad_filename_utf8_encoding (line 2192) | def test_download_bad_filename_utf8_encoding(self) -> None:
    method test_download_filename_string_encoding (line 2232) | def test_download_filename_string_encoding(self) -> None:
    method test_download_from_shared_library (line 2270) | def test_download_from_shared_library(self) -> None:
    method test_download_and_skip_old (line 2301) | def test_download_and_skip_old(self) -> None:
    method test_download_and_skip_new (line 2386) | def test_download_and_skip_new(self) -> None:
    method test_resume_download (line 2454) | def test_resume_download(self) -> None:

FILE: tests/test_download_photos_id.py
  class DownloadPhotoNameIDTestCase (line 30) | class DownloadPhotoNameIDTestCase(TestCase):
    method inject_fixtures (line 32) | def inject_fixtures(self) -> None:
    method test_download_and_skip_existing_photos_name_id7 (line 36) | def test_download_and_skip_existing_photos_name_id7(self) -> None:
    method test_download_photos_and_set_exif_name_id7 (line 113) | def test_download_photos_and_set_exif_name_id7(self) -> None:
    method test_download_photos_and_get_exif_exceptions_name_id7 (line 190) | def test_download_photos_and_get_exif_exceptions_name_id7(self) -> None:
    method test_skip_existing_downloads_name_id7 (line 241) | def test_skip_existing_downloads_name_id7(self) -> None:
    method test_until_found_name_id7 (line 287) | def test_until_found_name_id7(self) -> None:
    method test_handle_io_error_name_id7 (line 384) | def test_handle_io_error_name_id7(self) -> None:
    method test_handle_session_error_during_download_name_id7 (line 427) | def test_handle_session_error_during_download_name_id7(self) -> None:
    method test_handle_session_error_during_photo_iteration_name_id7 (line 488) | def test_handle_session_error_during_photo_iteration_name_id7(self) ->...
    method test_handle_albums_error_name_id7 (line 549) | def test_handle_albums_error_name_id7(self) -> None:
    method test_missing_size_name_id7 (line 593) | def test_missing_size_name_id7(self) -> None:
    method test_size_fallback_to_original_name_id7 (line 668) | def test_size_fallback_to_original_name_id7(self) -> None:
    method test_adjusted_size_fallback_to_original_name_id7 (line 724) | def test_adjusted_size_fallback_to_original_name_id7(self) -> None:
    method test_force_size_name_id7 (line 786) | def test_force_size_name_id7(self) -> None:
    method test_invalid_creation_date_name_id7 (line 838) | def test_invalid_creation_date_name_id7(self) -> None:
    method test_creation_date_without_century_name_id7 (line 894) | def test_creation_date_without_century_name_id7(self) -> None:
    method test_creation_date_prior_1970_name_id7 (line 948) | def test_creation_date_prior_1970_name_id7(self) -> None:
    method test_missing_item_type_name_id7 (line 998) | def test_missing_item_type_name_id7(self) -> None:
    method test_missing_item_type_value_name_id7 (line 1028) | def test_missing_item_type_value_name_id7(self) -> None:
    method test_download_and_dedupe_existing_photos_name_id7 (line 1058) | def test_download_and_dedupe_existing_photos_name_id7(self) -> None:
    method test_download_photos_and_set_exif_exceptions_name_id7 (line 1127) | def test_download_photos_and_set_exif_exceptions_name_id7(self) -> None:
    method test_download_chinese_name_id7 (line 1181) | def test_download_chinese_name_id7(self) -> None:
    method test_download_one_recent_live_photo_name_id7 (line 1229) | def test_download_one_recent_live_photo_name_id7(self) -> None:
    method test_download_one_recent_live_photo_chinese_name_id7 (line 1284) | def test_download_one_recent_live_photo_chinese_name_id7(self) -> None:
    method test_download_and_delete_after_name_id7 (line 1341) | def test_download_and_delete_after_name_id7(self) -> None:
    method test_download_and_not_delete_after_when_exists_name_id7 (line 1383) | def test_download_and_not_delete_after_when_exists_name_id7(self) -> N...
    method test_download_and_delete_after_fail_name_id7 (line 1427) | def test_download_and_delete_after_fail_name_id7(self) -> None:
    method test_download_over_old_original_photos_name_id7 (line 1463) | def test_download_over_old_original_photos_name_id7(self) -> None:
    method test_download_normalized_names_name_id7 (line 1524) | def test_download_normalized_names_name_id7(self) -> None:
    method test_handle_internal_error_during_download_name_id7 (line 1563) | def test_handle_internal_error_during_download_name_id7(self) -> None:
    method test_handle_internal_error_during_photo_iteration_name_id7 (line 1612) | def test_handle_internal_error_during_photo_iteration_name_id7(self) -...
    method test_handle_io_error_mkdir_name_id7 (line 1661) | def test_handle_io_error_mkdir_name_id7(self) -> None:
    method test_dry_run_name_id7 (line 1695) | def test_dry_run_name_id7(self) -> None:
    method test_download_after_delete_dry_run_name_id7 (line 1739) | def test_download_after_delete_dry_run_name_id7(self) -> None:
    method test_download_raw_photos_name_id7 (line 1792) | def test_download_raw_photos_name_id7(self) -> None:
    method test_download_two_sizes_name_id7 (line 1834) | def test_download_two_sizes_name_id7(self) -> None:
    method test_download_raw_alt_photos_name_id7 (line 1880) | def test_download_raw_alt_photos_name_id7(self) -> None:
    method test_download_raw_photos_policy_alt_with_adj_name_id7 (line 1930) | def test_download_raw_photos_policy_alt_with_adj_name_id7(self) -> None:
    method test_download_raw_photos_policy_orig_name_id7 (line 1978) | def test_download_raw_photos_policy_orig_name_id7(self) -> None:
    method test_download_raw_photos_policy_as_is_name_id7 (line 2028) | def test_download_raw_photos_policy_as_is_name_id7(self) -> None:
    method test_download_bad_filename_base64_encoding_name_id7 (line 2078) | def test_download_bad_filename_base64_encoding_name_id7(self) -> None:
    method test_download_bad_filename_utf8_encoding_name_id7 (line 2118) | def test_download_bad_filename_utf8_encoding_name_id7(self) -> None:
    method test_download_filename_string_encoding_name_id7 (line 2158) | def test_download_filename_string_encoding_name_id7(self) -> None:
    method test_download_and_skip_old_name_id7 (line 2196) | def test_download_and_skip_old_name_id7(self) -> None:
    method test_download_and_skip_new_name_id7 (line 2283) | def test_download_and_skip_new_name_id7(self) -> None:

FILE: tests/test_download_videos.py
  class DownloadVideoTestCase (line 13) | class DownloadVideoTestCase(TestCase):
    method inject_fixtures (line 15) | def inject_fixtures(self) -> None:
    method test_download_and_skip_existing_videos (line 19) | def test_download_and_skip_existing_videos(self) -> None:

FILE: tests/test_email_notifications.py
  class EmailNotificationsTestCase (line 12) | class EmailNotificationsTestCase(TestCase):
    method inject_fixtures (line 14) | def inject_fixtures(self) -> None:
    method test_2sa_required_email_notification (line 20) | def test_2sa_required_email_notification(self) -> None:
    method test_2sa_notification_without_smtp_login_and_tls (line 68) | def test_2sa_notification_without_smtp_login_and_tls(self) -> None:
    method test_2sa_required_notification_script (line 112) | def test_2sa_required_notification_script(self) -> None:
    method test_2sa_required_email_notification_from (line 141) | def test_2sa_required_email_notification_from(self) -> None:

FILE: tests/test_filenames.py
  function create_mock_photo_asset (line 11) | def create_mock_photo_asset(base_filename: str = "IMG_1") -> Any:
  class PathsTestCase (line 43) | class PathsTestCase(TestCase):
    method test_disambiguate_filenames_all_diff (line 44) | def test_disambiguate_filenames_all_diff(self) -> None:
    method test_disambiguate_filenames_keep_orgraw_alt_adj (line 78) | def test_disambiguate_filenames_keep_orgraw_alt_adj(self) -> None:
    method test_disambiguate_filenames_keep_latest (line 119) | def test_disambiguate_filenames_keep_latest(self) -> None:
    method test_disambiguate_filenames_keep_org_adj_diff (line 147) | def test_disambiguate_filenames_keep_org_adj_diff(self) -> None:
    method test_disambiguate_filenames_keep_org_alt_diff (line 179) | def test_disambiguate_filenames_keep_org_alt_diff(self) -> None:
    method test_disambiguate_filenames_keep_all_when_org_adj_same (line 211) | def test_disambiguate_filenames_keep_all_when_org_adj_same(self) -> None:
    method test_disambiguate_filenames_keep_org_alt_missing (line 256) | def test_disambiguate_filenames_keep_org_alt_missing(self) -> None:
    method test_disambiguate_filenames_keep_alt_missing (line 287) | def test_disambiguate_filenames_keep_alt_missing(self) -> None:
    method test_disambiguate_filenames_keep_adj_alt_missing (line 314) | def test_disambiguate_filenames_keep_adj_alt_missing(self) -> None:

FILE: tests/test_folder_structure.py
  class FolderStructureTestCase (line 15) | class FolderStructureTestCase(TestCase):
    method inject_fixtures (line 17) | def inject_fixtures(self) -> None:
    method test_default_folder_structure (line 22) | def test_default_folder_structure(self) -> None:
    method test_folder_structure_none (line 81) | def test_folder_structure_none(self) -> None:
    method test_folder_structure_de_posix (line 125) | def test_folder_structure_de_posix(self) -> None:
    method test_folder_structure_bad_format (line 181) | def test_folder_structure_bad_format(self) -> None:

FILE: tests/test_http.py
  class TestHttpUtils (line 16) | class TestHttpUtils(unittest.TestCase):
    method test_is_streaming_response_with_closed_connection (line 19) | def test_is_streaming_response_with_closed_connection(self) -> None:
    method test_is_streaming_response_with_open_connection (line 29) | def test_is_streaming_response_with_open_connection(self) -> None:
    method test_is_streaming_response_with_no_raw_attribute (line 39) | def test_is_streaming_response_with_no_raw_attribute(self) -> None:
    method test_is_streaming_response_with_exception (line 48) | def test_is_streaming_response_with_exception(self) -> None:
    method test_response_to_har_entry_with_json_response (line 58) | def test_response_to_har_entry_with_json_response(self) -> None:
    method test_response_to_har_entry_with_stream_response (line 82) | def test_response_to_har_entry_with_stream_response(self) -> None:
  class HttpTestRequestHandler (line 106) | class HttpTestRequestHandler(BaseHTTPRequestHandler):
    method log_message (line 109) | def log_message(self, format: str, *args: Any) -> None:  # noqa: ARG002
    method do_GET (line 113) | def do_GET(self) -> None:
  class TestHttpIntegration (line 184) | class TestHttpIntegration(unittest.TestCase):
    method setUpClass (line 193) | def setUpClass(cls) -> None:
    method tearDownClass (line 208) | def tearDownClass(cls) -> None:
    method test_response_to_har_entry_with_real_json_response (line 213) | def test_response_to_har_entry_with_real_json_response(self) -> None:
    method test_response_to_har_entry_with_real_text_response (line 238) | def test_response_to_har_entry_with_real_text_response(self) -> None:
    method test_response_to_har_entry_with_real_streaming_response (line 263) | def test_response_to_har_entry_with_real_streaming_response(self) -> N...
    method test_streaming_vs_non_streaming_behavior (line 290) | def test_streaming_vs_non_streaming_behavior(self) -> None:
    method test_response_to_har_entry_with_large_streaming_response (line 312) | def test_response_to_har_entry_with_large_streaming_response(self) -> ...

FILE: tests/test_issue_1220_only_print_filenames_dedup_bug.py
  class Issue1220OnlyPrintFilenamesDeduplicationBugTest (line 25) | class Issue1220OnlyPrintFilenamesDeduplicationBugTest(TestCase):
    method inject_fixtures (line 29) | def inject_fixtures(self) -> None:
    method test_only_print_filenames_should_not_download_during_deduplication (line 33) | def test_only_print_filenames_should_not_download_during_deduplication...

FILE: tests/test_json_rules.py
  class AppleRuleTestCase (line 7) | class AppleRuleTestCase(TestCase):
    method test_str_match (line 8) | def test_str_match(self) -> None:
    method test_str_not_match (line 15) | def test_str_not_match(self) -> None:
    method test_str_match_for_none (line 22) | def test_str_match_for_none(self) -> None:
    method test_tuple_match (line 29) | def test_tuple_match(self) -> None:
    method test_list_match (line 35) | def test_list_match(self) -> None:
    method test_list_match_each (line 41) | def test_list_match_each(self) -> None:
    method test_json_match (line 47) | def test_json_match(self) -> None:
    method test_json_match_by_leaf (line 53) | def test_json_match_by_leaf(self) -> None:
    method test_json_update_object_in_list (line 59) | def test_json_update_object_in_list(self) -> None:
    method test_json_drop_object_in_list (line 65) | def test_json_drop_object_in_list(self) -> None:
    method test_json_dict_drop (line 71) | def test_json_dict_drop(self) -> None:
    method test_json_list_drop (line 77) | def test_json_list_drop(self) -> None:

FILE: tests/test_keep_icloud_mode.py
  class KeepICloudModeTestCases (line 11) | class KeepICloudModeTestCases(TestCase):
    method inject_fixtures (line 13) | def inject_fixtures(self) -> None:
    method test_wide_range_keep_icloud_recent_days (line 17) | def test_wide_range_keep_icloud_recent_days(self) -> None:
    method test_narrow_range_keep_icloud_recent_days (line 62) | def test_narrow_range_keep_icloud_recent_days(self) -> None:
    method test_keep_icloud_recent_days_delete_all (line 106) | def test_keep_icloud_recent_days_delete_all(self) -> None:
    method test_keep_icloud_recent_days_1_keeps_today (line 151) | def test_keep_icloud_recent_days_1_keeps_today(self) -> None:
    method test_keep_icloud_recent_days_delete_existing (line 195) | def test_keep_icloud_recent_days_delete_existing(self) -> None:
    method test_keep_icloud_recent_days_keeps_some (line 251) | def test_keep_icloud_recent_days_keeps_some(self) -> None:
    method test_keep_icloud_recent_days_delete_existing_dry_run (line 307) | def test_keep_icloud_recent_days_delete_existing_dry_run(self) -> None:
    method test_keep_icloud_recent_days_with_skip_videos (line 353) | def test_keep_icloud_recent_days_with_skip_videos(self) -> None:

FILE: tests/test_listing_albums.py
  class ListingAlbumsTestCase (line 13) | class ListingAlbumsTestCase(TestCase):
    method inject_fixtures (line 15) | def inject_fixtures(self) -> None:
    method test_listing_albums (line 19) | def test_listing_albums(self) -> None:

FILE: tests/test_listing_libraries.py
  class ListingLibraryTestCase (line 13) | class ListingLibraryTestCase(TestCase):
    method inject_fixtures (line 15) | def inject_fixtures(self) -> None:
    method test_listing_library (line 19) | def test_listing_library(self) -> None:
    method test_listing_library_error (line 43) | def test_listing_library_error(self) -> None:

FILE: tests/test_listing_recent_photos.py
  class ListingRecentPhotosTestCase (line 14) | class ListingRecentPhotosTestCase(TestCase):
    method inject_fixtures (line 16) | def inject_fixtures(self) -> None:
    method test_listing_recent_photos (line 20) | def test_listing_recent_photos(self) -> None:
    method test_listing_photos_does_not_create_folders (line 72) | def test_listing_photos_does_not_create_folders(self) -> None:
    method test_listing_recent_photos_with_missing_filenameEnc (line 99) | def test_listing_recent_photos_with_missing_filenameEnc(self) -> None:
    method test_listing_recent_photos_with_missing_downloadURL (line 147) | def test_listing_recent_photos_with_missing_downloadURL(self) -> None:

FILE: tests/test_logger.py
  class LoggerTestCase (line 12) | class LoggerTestCase(TestCase):
    method test_logger_output (line 15) | def test_logger_output(self) -> None:
    method test_logger_tqdm_fallback (line 38) | def test_logger_tqdm_fallback(self) -> None:

FILE: tests/test_session_connection_errors.py
  class ConnectionTestHandler (line 21) | class ConnectionTestHandler(BaseHTTPRequestHandler):
    method do_GET (line 26) | def do_GET(self) -> None:
    method do_POST (line 47) | def do_POST(self) -> None:
    method log_message (line 51) | def log_message(self, format: str, *args: Any) -> None:  # noqa: ARG002
  class TestPyiCloudSessionConnectionErrors (line 56) | class TestPyiCloudSessionConnectionErrors(unittest.TestCase):
    method setUp (line 59) | def setUp(self) -> None:
    method tearDown (line 81) | def tearDown(self) -> None:
    method _find_free_port (line 88) | def _find_free_port(self) -> int:
    method _start_server (line 96) | def _start_server(self, error_type: str = "normal") -> None:
    method test_normal_request (line 106) | def test_normal_request(self) -> None:
    method test_connection_error_handling (line 113) | def test_connection_error_handling(self) -> None:
    method test_timeout_error_handling (line 123) | def test_timeout_error_handling(self) -> None:
    method test_builtin_timeout_error_handling (line 138) | def test_builtin_timeout_error_handling(self) -> None:
    method test_new_connection_error_handling (line 150) | def test_new_connection_error_handling(self) -> None:
    method test_other_exceptions_pass_through (line 164) | def test_other_exceptions_pass_through(self) -> None:
    method test_all_http_methods_covered (line 177) | def test_all_http_methods_covered(self) -> None:

FILE: tests/test_string_helpers.py
  class TruncateMiddleTestCase (line 12) | class TruncateMiddleTestCase(TestCase):
    method test_truncate_middle (line 13) | def test_truncate_middle(self) -> None:
  class ParseTimestampeOrTimeDeltaTestCase (line 28) | class ParseTimestampeOrTimeDeltaTestCase(TestCase):
    method test_totality (line 29) | def test_totality(self) -> None:
    method test_naive (line 37) | def test_naive(self) -> None:
    method test_naive_311plus (line 51) | def test_naive_311plus(self) -> None:
    method test_aware (line 57) | def test_aware(self) -> None:
    method test_aware_311plus (line 70) | def test_aware_311plus(self) -> None:
    method test_timestamp_invalid (line 79) | def test_timestamp_invalid(self) -> None:
    method test_delta_totality (line 83) | def test_delta_totality(self) -> None:
    method test_delta (line 94) | def test_delta(self) -> None:
    method test_delta_invalid (line 98) | def test_delta_invalid(self) -> None:

FILE: tests/test_two_step_auth.py
  class TwoStepAuthTestCase (line 17) | class TwoStepAuthTestCase(TestCase):
    method inject_fixtures (line 19) | def inject_fixtures(self) -> None:
    method test_2sa_flow_invalid_code (line 24) | def test_2sa_flow_invalid_code(self) -> None:
    method test_2sa_flow_valid_code (line 52) | def test_2sa_flow_valid_code(self) -> None:
    method test_2sa_flow_failed_send_code (line 88) | def test_2sa_flow_failed_send_code(self) -> None:
    method test_2fa_flow_invalid_code (line 124) | def test_2fa_flow_invalid_code(self) -> None:
    method test_2fa_flow_valid_code (line 152) | def test_2fa_flow_valid_code(self) -> None:
    method test_2fa_flow_valid_code_zero_lead (line 189) | def test_2fa_flow_valid_code_zero_lead(self) -> None:

FILE: tests/test_xmp_sidecar.py
  class BuildXMPMetadata (line 9) | class BuildXMPMetadata(TestCase):
    method test_build_metadata (line 10) | def test_build_metadata(self) -> None:
Copy disabled (too large) Download .json
Condensed preview — 224 files, each showing path, character count, and a content snippet. Download the .json file for the full structured content (40,741K chars).
[
  {
    "path": ".devcontainer/node/devcontainer.json",
    "chars": 900,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
  },
  {
    "path": ".devcontainer/python/devcontainer.json",
    "chars": 1110,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the\n// README at: https://github.co"
  },
  {
    "path": ".dockerignore",
    "chars": 227,
    "preview": "**/.DS_Store\nbuild/\nhtmlcov/\nicloudpd.egg-info\ncron_script.sh\n.vscode\n**/__pycache__\n.pytest_cache/\n.cache\n*.pyc\n.idea\n."
  },
  {
    "path": ".gitattributes",
    "chars": 27,
    "preview": "* text eol=lf\n*.png binary\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "chars": 696,
    "preview": "---\nname: Bug Report\nabout: provide information about a problem\ntitle: \nlabels: bug\nassignees: ''\n\n---\n## Overview\n[TIP]"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "chars": 338,
    "preview": "---\nname: Generic Ticket\nabout: generic/empty ticket for discussion/questions that are not bugs or enhancements\ntitle: \n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/generic.md",
    "chars": 322,
    "preview": "---\nname: Feature Request\nabout: tell us about a new feature or improvement you want\ntitle: \nlabels: enhancement\nassigne"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 535,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/build-docs.yml",
    "chars": 1786,
    "preview": "name: \"Render docs\"\n\non: workflow_call\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkou"
  },
  {
    "path": ".github/workflows/build-package.yml",
    "chars": 132354,
    "preview": "\nname: Build and Package\n\non:\n  workflow_call:\n    inputs:\n      icloudpd_version:\n        required: true\n        type: "
  },
  {
    "path": ".github/workflows/codeql-analysis.yml",
    "chars": 2352,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/compile-notes.yml",
    "chars": 938,
    "preview": "\nname: Compile Release Notes\n\non:\n  workflow_call:\n    inputs:\n      icloudpd_version:\n        required: true\n        ty"
  },
  {
    "path": ".github/workflows/create-release.yml",
    "chars": 1787,
    "preview": "on:\n  push:\n    # Sequence of patterns matched against refs/tags\n    tags:\n      - 'v*' # Push events to matching v*, i."
  },
  {
    "path": ".github/workflows/extract-changelog.yml",
    "chars": 755,
    "preview": "\nname: Extract Changelog\n\non:\n  workflow_call:\n    inputs:\n      icloudpd_version:\n        required: true\n        type: "
  },
  {
    "path": ".github/workflows/patch-version.yml",
    "chars": 412,
    "preview": "\nname: Patch Version\n\non:\n  workflow_call:\n\njobs:\n\n  patch_version:\n    runs-on: ubuntu-22.04\n    steps:\n\n    - uses: ac"
  },
  {
    "path": ".github/workflows/produce-artifacts.yml",
    "chars": 2279,
    "preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
  },
  {
    "path": ".github/workflows/publish-docs.yml",
    "chars": 454,
    "preview": "name: \"Publish docs to GH pages\"\n\non: workflow_call\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHu"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 5105,
    "preview": "on:\n  workflow_call:\n    inputs:\n      icloudpd_version:\n        required: true\n        type: string\n      PYPI_REPOSITO"
  },
  {
    "path": ".github/workflows/quality-checks.yml",
    "chars": 5197,
    "preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
  },
  {
    "path": ".github/workflows/refresh-docs.yml",
    "chars": 506,
    "preview": "# This workflow will install Python dependencies, run tests and lint with a single version of Python\n# For more informat"
  },
  {
    "path": ".gitignore",
    "chars": 378,
    "preview": ".DS_Store\nbuild/\ndist/\nwheelhouse/\nicloudpd.egg-info\ntests/fixtures/**\n__pycache__\n.pytest_cache\n.cache\n*.pyc\n.idea\n.cov"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 119,
    "preview": "{\n    \"cSpell.words\": [\n        \"icloudpd\",\n        \"seealso\",\n        \"versionadded\",\n        \"versionchanged\"\n    ]\n}"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 23410,
    "preview": "# Change log\n\n## Unreleased\n\n## 1.32.2 (2025-09-01)\n\n- fix: HTTP response content not captured for authentication and no"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "chars": 3286,
    "preview": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 8848,
    "preview": "# Contributing iCloud Photos Downloader\n\n[//]: # (inspired from https://raw.githubusercontent.com/keepassxreboot/keepass"
  },
  {
    "path": "Dockerfile",
    "chars": 1643,
    "preview": "FROM alpine:3.18 AS runtime_amd64_none\nENV MUSL_LOCPATH=\"/usr/share/i18n/locales/musl\"\nRUN apk update && apk add --no-ca"
  },
  {
    "path": "Dockerfile.build",
    "chars": 3548,
    "preview": "# Multi-arch build (local):\n#     docker buildx create --use --driver=docker-container --name container --bootstrap\n#   "
  },
  {
    "path": "Dockerfile.build-doc",
    "chars": 906,
    "preview": "# build (local):\n#     docker buildx create --use --driver=docker-container --name container --bootstrap\n#     docker bu"
  },
  {
    "path": "Dockerfile.build-musl",
    "chars": 2112,
    "preview": "# Multi-arch build (local):\n#     docker buildx create --use --driver=docker-container --name container --bootstrap\n#   "
  },
  {
    "path": "EXPERIMENTAL.md",
    "chars": 273,
    "preview": "# Experimental Mode\n\nGoal is to try new things and get feedback from users without breaking existing behavior.\n\nAnything"
  },
  {
    "path": "LICENSE.md",
    "chars": 1083,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Nathan Broadbent\n\nPermission is hereby granted, free of charge, to any person "
  },
  {
    "path": "README.md",
    "chars": 4794,
    "preview": "# !!!! [Looking for MAINTAINER for this project](https://github.com/icloud-photos-downloader/icloud_photos_downloader/is"
  },
  {
    "path": "README_AUR.md",
    "chars": 1458,
    "preview": "# iCloud Photos Downloader [![Quality Checks](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workf"
  },
  {
    "path": "README_DOCKER.md",
    "chars": 1898,
    "preview": "# iCloud Photos Downloader [![Quality Checks](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workf"
  },
  {
    "path": "README_NPM.md",
    "chars": 1155,
    "preview": "# iCloud Photos Downloader [![Quality Checks](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workf"
  },
  {
    "path": "README_PYPI.md",
    "chars": 2333,
    "preview": "# iCloud Photos Downloader [![Quality Checks](https://github.com/icloud-photos-downloader/icloud_photos_downloader/workf"
  },
  {
    "path": "binary_dist/pyproject.toml",
    "chars": 980,
    "preview": "[build-system]\nrequires = [\n    \"setuptools>=75.6.0,<76\",\n    \"wheel>=0.45.1,<0.46\",\n]\nbuild-backend = \"setuptools.build"
  },
  {
    "path": "binary_dist/src/icloud/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "binary_dist/src/icloud/__main__.py",
    "chars": 134,
    "preview": "import os\nimport subprocess\nimport sys\n\nsys.exit(subprocess.call([os.path.join(os.path.dirname(__file__), \"icloud\"), *sy"
  },
  {
    "path": "binary_dist/src/icloudpd/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "binary_dist/src/icloudpd/__main__.py",
    "chars": 136,
    "preview": "import os\nimport subprocess\nimport sys\n\nsys.exit(subprocess.call([os.path.join(os.path.dirname(__file__), \"icloudpd\"), *"
  },
  {
    "path": "docs/authentication.md",
    "chars": 5360,
    "preview": "# iCloud Authentication\n\n## Multi-Factor Authentication\n\nIf your Apple account has two-factor authentication (multi-fact"
  },
  {
    "path": "docs/conf.py",
    "chars": 1093,
    "preview": "# Configuration file for the Sphinx documentation builder.\n#\n# For the full list of built-in configuration values, see t"
  },
  {
    "path": "docs/index.md",
    "chars": 291,
    "preview": "# iCloud Photos Downloader\n\nA command-line tool to download your iCloud photos to local storage\n\n```{include} ../README."
  },
  {
    "path": "docs/install.md",
    "chars": 4461,
    "preview": "# Install and Run\n\nThere are three ways to run `icloudpd`:\n1. Download executable for your platform from the GitHub [Rel"
  },
  {
    "path": "docs/mode.md",
    "chars": 812,
    "preview": "# Operation Modes\n\n```{versionchanged} 1.8.0\nAdded `--delete-after-download` parameter\n```\n\n`icloudpd` works in one of t"
  },
  {
    "path": "docs/naming.md",
    "chars": 3061,
    "preview": "# File Naming\n\nAssets on iCloud have names. When downloading assets, `icloudpd` can adjust names.\n\n(folder-structure)=\n#"
  },
  {
    "path": "docs/nas.md",
    "chars": 3236,
    "preview": "# Network Attached Storage\n\nThe following are example setups for NAS.\n\n## TrueNAS\n\nUse the [`Install Custom App` button]"
  },
  {
    "path": "docs/raw.md",
    "chars": 1342,
    "preview": "# RAW Assets\n\n## Apple ProRAW/ProRes\n\nApple supports shooting stills and videos in [DNG](https://en.wikipedia.org/wiki/D"
  },
  {
    "path": "docs/reference.md",
    "chars": 11375,
    "preview": "# Reference\n\nThis is a list of all options available for the command line interface (CLI) of `icloudpd`\n\n(until-found-pa"
  },
  {
    "path": "docs/size.md",
    "chars": 1171,
    "preview": "# Asset Sizes\n\n## Basic Sizes\n\n```{versionchanged} 1.19.0\nMultiple `--size` parameters can be specified\n```\n\nEach asset "
  },
  {
    "path": "docs/webui.md",
    "chars": 288,
    "preview": "# Web UI\n\n```{versionadded} 1.21.0\n```\n\n`icloudpd` can start an internal web server on port 8080 and accept input (passw"
  },
  {
    "path": "examples/cron_script.sh.example",
    "chars": 242,
    "preview": "#!/bin/bash\n# Make sure it's not already running\npgrep -f icloudpd && echo \"icloudpd is already running.\" && exit\n\niclou"
  },
  {
    "path": "help_current.txt",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "jq/version.jq",
    "chars": 89,
    "preview": ".version |= $version  | (.optionalDependencies |= with_entries(.value |= $version))? // ."
  },
  {
    "path": "npm/@icloudpd/darwin-arm64/README.md",
    "chars": 164,
    "preview": "# icloudpd\n\nThe macOS ARM 64-bit binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_dow"
  },
  {
    "path": "npm/@icloudpd/darwin-arm64/package.json",
    "chars": 460,
    "preview": "{\n    \"name\": \"@icloudpd/darwin-arm64\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The macOS ARM 64-bit binary for iclo"
  },
  {
    "path": "npm/@icloudpd/darwin-x64/README.md",
    "chars": 160,
    "preview": "# icloudpd\n\nThe macOS 64-bit binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_downloa"
  },
  {
    "path": "npm/@icloudpd/darwin-x64/package.json",
    "chars": 452,
    "preview": "{\n    \"name\": \"@icloudpd/darwin-x64\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The macOS 64-bit binary for icloudpd, "
  },
  {
    "path": "npm/@icloudpd/linux-arm/README.md",
    "chars": 160,
    "preview": "# icloudpd\n\nThe Linux ARM/v7 binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_downloa"
  },
  {
    "path": "npm/@icloudpd/linux-arm/package.json",
    "chars": 450,
    "preview": "{\n    \"name\": \"@icloudpd/linux-arm\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The Linux ARM/v7 binary for icloudpd, a"
  },
  {
    "path": "npm/@icloudpd/linux-arm64/README.md",
    "chars": 164,
    "preview": "# icloudpd\n\nThe Linux ARM 64-bit binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_dow"
  },
  {
    "path": "npm/@icloudpd/linux-arm64/package.json",
    "chars": 458,
    "preview": "{\n    \"name\": \"@icloudpd/linux-arm64\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The Linux ARM 64-bit binary for iclou"
  },
  {
    "path": "npm/@icloudpd/linux-x64/README.md",
    "chars": 160,
    "preview": "# icloudpd\n\nThe Linux 64-bit binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_downloa"
  },
  {
    "path": "npm/@icloudpd/linux-x64/package.json",
    "chars": 450,
    "preview": "{\n    \"name\": \"@icloudpd/linux-x64\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The Linux 64-bit binary for icloudpd, a"
  },
  {
    "path": "npm/@icloudpd/win32-x64/README.md",
    "chars": 162,
    "preview": "# icloudpd\n\nThe Windows 64-bit binary for icloudpd, a iCloud Photo Downloader. See https://github.com/icloud_photo_downl"
  },
  {
    "path": "npm/@icloudpd/win32-x64/package.json",
    "chars": 452,
    "preview": "{\n    \"name\": \"@icloudpd/win32-x64\",\n    \"version\": \"0.0.1\",\n    \"description\": \"The Windows 64-bit binary for icloudpd,"
  },
  {
    "path": "npm/icloudpd/bin/icloudpd.js",
    "chars": 1191,
    "preview": "#!/usr/bin/env node\n\"use strict\";\nvar os = require(\"os\");\nvar fs = require(\"fs\");\nvar platformKey = `${process.platform}"
  },
  {
    "path": "npm/icloudpd/package.json",
    "chars": 941,
    "preview": "{\n  \"name\": \"icloudpd\",\n  \"version\": \"0.0.1\",\n  \"description\": \"iCloud Photo Downloader\",\n  \"engines\": {\n    \"node\": \">="
  },
  {
    "path": "npm/icloudpd/preinstall.js",
    "chars": 969,
    "preview": "#!/usr/bin/env node\n\"use strict\";\nvar os = require(\"os\");\nvar fs = require(\"fs\");\nvar platformKey = `${process.platform}"
  },
  {
    "path": "pyproject.toml",
    "chars": 2804,
    "preview": "[build-system]\nrequires = [\n    \"setuptools==80.9.0\",\n    \"wheel==0.45.1\",\n]\nbuild-backend = \"setuptools.build_meta\"\n\n[p"
  },
  {
    "path": "requirements-pip.txt",
    "chars": 10,
    "preview": "pip>=25.1\n"
  },
  {
    "path": "scripts/build",
    "chars": 129,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nmkdir -p dist\nrm -fr dist/*\npython3 -m pip wheel . -w dist --no-deps --no-build-i"
  },
  {
    "path": "scripts/build_bin1",
    "chars": 425,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare bin package in dist\n# expects python with installed dependencies\n# requ"
  },
  {
    "path": "scripts/build_binary_dist_macos",
    "chars": 650,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare binary wheels in dist\n# expects dist to have comipled binary versions\n#"
  },
  {
    "path": "scripts/build_binary_dist_windows",
    "chars": 749,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare binary wheels in dist\n# expects dist to have comipled binary versions\n#"
  },
  {
    "path": "scripts/build_npm",
    "chars": 2610,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare npm packages in dist\n# expects dist to have comipled binary versions\n# "
  },
  {
    "path": "scripts/build_static",
    "chars": 288,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare static bin package in dist\n# expects staticx installed dependencies\n# r"
  },
  {
    "path": "scripts/build_whl",
    "chars": 643,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# prepare binary wheels in dist\n# expects dist to have comipled binary versions\n#"
  },
  {
    "path": "scripts/clean",
    "chars": 54,
    "preview": "#!/usr/bin/env sh\nset -euo pipefail\nrm -rf build dist\n"
  },
  {
    "path": "scripts/clone_whl_version",
    "chars": 781,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# creates new versions of all whl in dist folder\n# param: from_version to_version"
  },
  {
    "path": "scripts/compile_compatibility.py",
    "chars": 1014,
    "preview": "#!/usr/bin/env python3\n\"\"\"takes results of compatibility tests and compie into one file\"\"\"\n\nimport os\nimport sys\n\nfrom c"
  },
  {
    "path": "scripts/compile_matrix.py",
    "chars": 2185,
    "preview": "\"\"\"takes results of compatibility tests and compile into one file\"\"\"\n\nimport itertools\nimport os\n\n\ndef _stats(files):\n  "
  },
  {
    "path": "scripts/compile_tzlc.py",
    "chars": 1701,
    "preview": "#!/usr/bin/env python3\n\"\"\"takes results of tz and locale compatibility tests and compile into one file\"\"\"\n\nimport sys\n\nf"
  },
  {
    "path": "scripts/extract_releasenotes",
    "chars": 239,
    "preview": "#!/usr/bin/env python3\n\nimport sys\n\nwith open(sys.argv[1]) as file:\n  releases = 0\n\n  for i, line in enumerate(file):\n  "
  },
  {
    "path": "scripts/format",
    "chars": 182,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\necho \"Formatting...\"\n# autopep8 -r --in-place --aggressive --aggressive src/ --exc"
  },
  {
    "path": "scripts/get_version",
    "chars": 99,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\necho $(cat pyproject.toml | grep version= | cut -d'\"' -f 2)\n"
  },
  {
    "path": "scripts/install_deps",
    "chars": 278,
    "preview": "#!/usr/bin/env bash\n# this is used to setup development env (all required dependencies for all cases)\nset -euo pipefail\n"
  },
  {
    "path": "scripts/lint",
    "chars": 83,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\necho \"Linting...\"\nruff check --ignore \"E402\"\n"
  },
  {
    "path": "scripts/npx_optional",
    "chars": 177,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [ `which npx` ]; then\n    echo \"test icloudpd...\" &&\n    npx ${@:3} 1>$1 &&\n  "
  },
  {
    "path": "scripts/npx_optional_touch",
    "chars": 174,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nif [ `which npx` ]; then\n    echo \"test icloudpd...\" &&\n    npx ${@:3} &&\n    tou"
  },
  {
    "path": "scripts/patch_version",
    "chars": 371,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\nVERSION=$(cat pyproject.toml | grep version= | cut -d'\"' -f 2)\nSHA=$(git rev-pars"
  },
  {
    "path": "scripts/publish_npm",
    "chars": 616,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# dev script\n# expecting dist/npm to be set\n# required param registry_url\n#\n# pre"
  },
  {
    "path": "scripts/publish_pypi",
    "chars": 513,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# dev script\n# expecting dist/binary_dist to be set\n# required param registry_url"
  },
  {
    "path": "scripts/run_all_checks",
    "chars": 306,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# realpath not available on Mac by default...\nrealpath() {\n    [[ $1 = /* ]] && e"
  },
  {
    "path": "scripts/test",
    "chars": 475,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# realpath not available on Mac by default...\nrealpath() {\n    [[ $1 = /* ]] && e"
  },
  {
    "path": "scripts/test_python_versions.sh",
    "chars": 8872,
    "preview": "#!/bin/bash\nset -e\n\n# Script to test icloudpd across multiple Python versions using Docker\n\nSUPPORTED_VERSIONS=(\"3.10\" \""
  },
  {
    "path": "scripts/type_check",
    "chars": 250,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\necho \"Running mypy...\"\npython3 -m mypy src tests --strict --python-version 3.10\n# "
  },
  {
    "path": "scripts/unpublish_npm",
    "chars": 433,
    "preview": "#!/usr/bin/env bash\nset -euo pipefail\n\n# dev script\n# required param: registry_url\n#\n# note that verdaccio v5 is flaky a"
  },
  {
    "path": "src/foundation/__init__.py",
    "chars": 3562,
    "preview": "import datetime\nfrom functools import partial\nfrom operator import is_, not_\nfrom typing import (\n    Callable,\n    Cont"
  },
  {
    "path": "src/foundation/core/__init__.py",
    "chars": 8340,
    "preview": "from itertools import chain, islice, tee, zip_longest\nfrom typing import Callable, Iterable, Tuple, TypeGuard, TypeVar\n\n"
  },
  {
    "path": "src/foundation/core/optional/__init__.py",
    "chars": 3229,
    "preview": "from typing import Callable, TypeVar\n\n_Tin = TypeVar(\"_Tin\")\n_Tin2 = TypeVar(\"_Tin2\")\n_Tin3 = TypeVar(\"_Tin3\")\n_Tout = T"
  },
  {
    "path": "src/foundation/http.py",
    "chars": 3332,
    "preview": "import json\nfrom functools import partial\nfrom http.cookiejar import Cookie\nfrom http.cookies import SimpleCookie\nfrom o"
  },
  {
    "path": "src/foundation/json.py",
    "chars": 5131,
    "preview": "import re\nfrom functools import partial, singledispatch\nfrom operator import is_, not_\nfrom typing import Any, Callable,"
  },
  {
    "path": "src/foundation/predicates.py",
    "chars": 3190,
    "preview": "\"\"\"Boolean predicate composition utilities for functional programming\"\"\"\n\nfrom typing import Callable, TypeVar\n\nT = Type"
  },
  {
    "path": "src/foundation/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/foundation/string.py",
    "chars": 279,
    "preview": "import binascii\nfrom functools import singledispatch\nfrom typing import Any\n\n\n@singledispatch\ndef obfuscate(_input: Any)"
  },
  {
    "path": "src/foundation/string_utils.py",
    "chars": 3539,
    "preview": "\"\"\"Point-free string utility functions for functional composition\"\"\"\n\nfrom typing import Callable\n\nfrom foundation.core "
  },
  {
    "path": "src/icloudpd/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/icloudpd/authentication.py",
    "chars": 10816,
    "preview": "\"\"\"Handles username/password authentication and two-step authentication\"\"\"\n\nimport logging\nimport sys\nimport time\nfrom f"
  },
  {
    "path": "src/icloudpd/autodelete.py",
    "chars": 4495,
    "preview": "\"\"\"\nDelete any files found in \"Recently Deleted\"\n\"\"\"\n\nimport datetime\nimport logging\nimport os\nfrom typing import Callab"
  },
  {
    "path": "src/icloudpd/base.py",
    "chars": 51061,
    "preview": "#!/usr/bin/env python\n\"\"\"Main script that uses Click to parse command-line arguments\"\"\"\n\nimport datetime\nimport getpass\n"
  },
  {
    "path": "src/icloudpd/cli.py",
    "chars": 22531,
    "preview": "import argparse\nimport copy\nimport datetime\nimport pathlib\nimport sys\nfrom itertools import dropwhile\nfrom operator impo"
  },
  {
    "path": "src/icloudpd/config.py",
    "chars": 2114,
    "preview": "import datetime\nimport pathlib\nfrom dataclasses import dataclass\nfrom typing import Sequence\n\nfrom icloudpd.log_level im"
  },
  {
    "path": "src/icloudpd/constants.py",
    "chars": 152,
    "preview": "\"\"\"Constants\"\"\"\n\nfrom typing import Final\n\n# For retrying connection after timeouts and errors\nMAX_RETRIES: Final[int] ="
  },
  {
    "path": "src/icloudpd/counter.py",
    "chars": 517,
    "preview": "\"\"\"Atomic counter\"\"\"\n\nfrom multiprocessing import Lock, RawValue\n\n\nclass Counter:\n    def __init__(self, value: int = 0)"
  },
  {
    "path": "src/icloudpd/download.py",
    "chars": 7354,
    "preview": "\"\"\"Handles file downloads with retries and error handling\"\"\"\n\nimport base64\nimport datetime\nimport logging\nimport os\nimp"
  },
  {
    "path": "src/icloudpd/email_notifications.py",
    "chars": 1671,
    "preview": "\"\"\"Send an email notification when 2SA is expired\"\"\"\n\nimport datetime\nimport logging\nimport smtplib\nfrom typing import c"
  },
  {
    "path": "src/icloudpd/exif_datetime.py",
    "chars": 1073,
    "preview": "\"\"\"Get/set EXIF dates from photos\"\"\"\n\nimport logging\nimport typing\n\nimport piexif\nfrom piexif._exceptions import Invalid"
  },
  {
    "path": "src/icloudpd/filename_policies.py",
    "chars": 2007,
    "preview": "\"\"\"Filename building utilities with policy application\"\"\"\n\nfrom typing import Callable\n\nfrom foundation.core import part"
  },
  {
    "path": "src/icloudpd/log_level.py",
    "chars": 160,
    "preview": "from enum import Enum\n\n\nclass LogLevel(Enum):\n    DEBUG = \"debug\"\n    INFO = \"info\"\n    ERROR = \"error\"\n\n    def __str__"
  },
  {
    "path": "src/icloudpd/logger.py",
    "chars": 1768,
    "preview": "\"\"\"Custom logging class and setup function\"\"\"\n\nimport logging\nimport sys\nfrom logging import INFO\nfrom typing import Any"
  },
  {
    "path": "src/icloudpd/mfa_provider.py",
    "chars": 149,
    "preview": "from enum import Enum\n\n\nclass MFAProvider(Enum):\n    CONSOLE = \"console\"\n    WEBUI = \"webui\"\n\n    def __str__(self) -> s"
  },
  {
    "path": "src/icloudpd/password_provider.py",
    "chars": 206,
    "preview": "from enum import Enum\n\n\nclass PasswordProvider(Enum):\n    CONSOLE = \"console\"\n    WEBUI = \"webui\"\n    PARAMETER = \"param"
  },
  {
    "path": "src/icloudpd/paths.py",
    "chars": 651,
    "preview": "\"\"\"Path functions\"\"\"\n\nimport os\n\n\ndef remove_unicode_chars(value: str) -> str:\n    \"\"\"Removes unicode chars from the str"
  },
  {
    "path": "src/icloudpd/progress.py",
    "chars": 1577,
    "preview": "import datetime\n\n\nclass Progress:\n    def __init__(self) -> None:\n        self._photos_count = 0\n        self._photos_co"
  },
  {
    "path": "src/icloudpd/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/icloudpd/server/__init__.py",
    "chars": 3581,
    "preview": "import os\nimport sys\nfrom logging import Logger\n\nimport waitress\nfrom flask import Flask, Response, make_response, rende"
  },
  {
    "path": "src/icloudpd/server/static/js/toast.js",
    "chars": 275,
    "preview": "document.addEventListener(\"htmx:afterRequest\", function(evt) {\n    if (evt.detail.xhr.status >= 400 && evt.detail.xhr.st"
  },
  {
    "path": "src/icloudpd/server/static/manifest.json",
    "chars": 320,
    "preview": "{\n    \"version\": \"1.0.0\",\n    \"manifest_version\": 3,\n    \"name\": \"iCloud Photos Downloader\",\n    \"short_name\": \"icloudpd"
  },
  {
    "path": "src/icloudpd/server/static/style/style.css",
    "chars": 94,
    "preview": "input {\n    max-width: 350px;\n}\n\n.navbar-brand {\n    display: flex;\n    align-items: center;\n}"
  },
  {
    "path": "src/icloudpd/server/templates/auth_error.html",
    "chars": 256,
    "preview": "<div>\n    <fieldset>\n        <legend>Authentication{% if current_user %} - {{ current_user }}{% endif %}</legend>\n      "
  },
  {
    "path": "src/icloudpd/server/templates/code.html",
    "chars": 916,
    "preview": "<form hx-post=\"/code\" hx-swap=\"outerHTML\" class=\"row align-items-center\" hx-target-error=\"#toast-content\">\n    <fieldset"
  },
  {
    "path": "src/icloudpd/server/templates/code_submitted.html",
    "chars": 364,
    "preview": "<div id=\"zone\">\n    <div class=\"col-12\">\n        <label for=\"smt\" class=\"form-label\">Code submitted and being checked{% "
  },
  {
    "path": "src/icloudpd/server/templates/index.html",
    "chars": 2788,
    "preview": "<!doctype html>\n<head>\n    <title>iCloud Photos Downloader</title>\n    <link rel=\"manifest\" href=\"/static/manifest.json\""
  },
  {
    "path": "src/icloudpd/server/templates/no_input.html",
    "chars": 9126,
    "preview": "<div hx-get=\"/status\" hx-trigger=\"every 5s\" hx-swap=\"outerHTML\">\n\n    <div class=\"container-fluid\">\n        <div class=\""
  },
  {
    "path": "src/icloudpd/server/templates/password.html",
    "chars": 923,
    "preview": "<form hx-post=\"/password\" hx-swap=\"outerHTML\" class=\"row align-items-center\" hx-target-error=\"#toast-content\">\n    <fiel"
  },
  {
    "path": "src/icloudpd/server/templates/password_submitted.html",
    "chars": 367,
    "preview": "<div id=\"zone\">\n    <div class=\"col-12\">\n        <label for=\"smt\" class=\"form-label\">Password submitted and being checke"
  },
  {
    "path": "src/icloudpd/server/templates/status.html",
    "chars": 105,
    "preview": "<div hx-get=\"/status\" hx-trigger=\"every 10s\" hx-swap=\"outerHTML\">\n    <p>Status: {{ status }}</p>\n</div> "
  },
  {
    "path": "src/icloudpd/status.py",
    "chars": 3722,
    "preview": "from enum import Enum\nfrom threading import Lock\nfrom typing import Sequence\n\nfrom icloudpd.config import GlobalConfig, "
  },
  {
    "path": "src/icloudpd/string_helpers.py",
    "chars": 1749,
    "preview": "\"\"\"String helper functions\"\"\"\n\nimport datetime\nimport re\nfrom typing import Sequence\n\n\ndef truncate_middle(string: str, "
  },
  {
    "path": "src/icloudpd/xmp_sidecar.py",
    "chars": 12014,
    "preview": "\"\"\"Generate XMP sidecar file from photo asset record\"\"\"\n\nfrom __future__ import annotations\n\nimport base64\nimport json\ni"
  },
  {
    "path": "src/pyicloud_ipd/__init__.py",
    "chars": 144,
    "preview": "import logging\n\nfrom pyicloud_ipd.base import PyiCloudService as PyiCloudService\n\nlogging.getLogger(__name__).addHandler"
  },
  {
    "path": "src/pyicloud_ipd/asset_version.py",
    "chars": 4436,
    "preview": "import os\nfrom typing import Callable, Dict\n\nfrom pyicloud_ipd.item_type import AssetItemType\nfrom pyicloud_ipd.version_"
  },
  {
    "path": "src/pyicloud_ipd/base.py",
    "chars": 33951,
    "preview": "import base64\nimport getpass\nimport hashlib\nimport http.cookiejar as cookielib\nimport json\nimport logging\nimport typing\n"
  },
  {
    "path": "src/pyicloud_ipd/cmdline.py",
    "chars": 6000,
    "preview": "#! /usr/bin/env python\n\"\"\"\nA Command Line Wrapper to allow easy use of pyicloud for\ncommand line scripts, and related.\n\""
  },
  {
    "path": "src/pyicloud_ipd/exceptions.py",
    "chars": 2258,
    "preview": "class PyiCloudException(Exception):\n    \"\"\"Generic iCloud exception.\"\"\"\n\n    pass\n\n\n# API\nclass PyiCloudAPIResponseExcep"
  },
  {
    "path": "src/pyicloud_ipd/file_match.py",
    "chars": 199,
    "preview": "from enum import Enum\n\n\nclass FileMatchPolicy(Enum):\n    NAME_SIZE_DEDUP_WITH_SUFFIX = \"name-size-dedup-with-suffix\"\n   "
  },
  {
    "path": "src/pyicloud_ipd/item_type.py",
    "chars": 147,
    "preview": "from enum import Enum\n\n\nclass AssetItemType(Enum):\n    MOVIE = \"movie\"\n    IMAGE = \"image\"\n\n    def __str__(self) -> str"
  },
  {
    "path": "src/pyicloud_ipd/live_photo_mov_filename_policy.py",
    "chars": 168,
    "preview": "from enum import Enum\n\n\nclass LivePhotoMovFilenamePolicy(Enum):\n    SUFFIX = \"suffix\"\n    ORIGINAL = \"original\"\n\n    def"
  },
  {
    "path": "src/pyicloud_ipd/py.typed",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/pyicloud_ipd/raw_policy.py",
    "chars": 202,
    "preview": "from enum import Enum\n\n\nclass RawTreatmentPolicy(Enum):\n    AS_IS = \"as-is\"\n    AS_ORIGINAL = \"as-original\"\n    AS_ALTER"
  },
  {
    "path": "src/pyicloud_ipd/services/__init__.py",
    "chars": 72,
    "preview": "from pyicloud_ipd.services.photos import PhotosService as PhotosService\n"
  },
  {
    "path": "src/pyicloud_ipd/services/photos.py",
    "chars": 36044,
    "preview": "import base64\nimport copy\nimport json\nimport logging\nimport re\nimport typing\nfrom datetime import datetime\nfrom typing i"
  },
  {
    "path": "src/pyicloud_ipd/session.py",
    "chars": 6883,
    "preview": "import inspect\nimport json\nimport logging\nimport typing\nfrom typing import Any, Callable, Dict, Mapping, NoReturn, Seque"
  },
  {
    "path": "src/pyicloud_ipd/sms.py",
    "chars": 6115,
    "preview": "import json\nfrom html.parser import HTMLParser\nfrom typing import Any, List, Mapping, NamedTuple, Protocol, Sequence, Tu"
  },
  {
    "path": "src/pyicloud_ipd/utils.py",
    "chars": 8308,
    "preview": "import copy\nfrom typing import TYPE_CHECKING, Any, Callable, Dict, Sequence, Tuple, TypeVar\n\nimport keyring\nfrom request"
  },
  {
    "path": "src/pyicloud_ipd/version_size.py",
    "chars": 452,
    "preview": "from enum import Enum\n\n\nclass AssetVersionSize(Enum):\n    ORIGINAL = \"original\"\n    ADJUSTED = \"adjusted\"\n    ALTERNATIV"
  },
  {
    "path": "src/starters/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/starters/icloud.py",
    "chars": 99,
    "preview": "#!/usr/bin/env python\nfrom pyicloud_ipd.cmdline import main\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": "src/starters/icloudpd.py",
    "chars": 90,
    "preview": "#!/usr/bin/env python\n\nfrom icloudpd.cli import cli\n\nif __name__ == \"__main__\":\n    cli()\n"
  },
  {
    "path": "tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "tests/cookie/jdoegmailcom",
    "chars": 654,
    "preview": "#LWP-Cookies-2.0\nSet-Cookie3: X-APPLE-UNIQUE-CLIENT-ID=\"\\\"Cw==\\\"\"; path=\"/\"; domain=\".icloud.com\"; path_spec; domain_dot"
  },
  {
    "path": "tests/cookie/jdoegmailcom.session",
    "chars": 223,
    "preview": "{\"client_id\": \"DE309E26-942E-11E8-92F5-14109FE0B321\", \"account_country\": \"USA\", \"session_id\": \"sess-1234567890\", \"sessio"
  },
  {
    "path": "tests/data/parse_trusted_phone_numbers_payload_valid.html",
    "chars": 6886,
    "preview": "<script type=\"application/json\" class=\"pre\">{\"direct\":{\"scriptSk7Url\":\"https://appleid.cdn-apple.com/appleauth/static/mo"
  },
  {
    "path": "tests/helpers/__init__.py",
    "chars": 12311,
    "preview": "import glob\nimport io\nimport os\nimport shutil\nimport sys\nimport traceback\nfrom contextlib import redirect_stderr, redire"
  },
  {
    "path": "tests/test_authentication.py",
    "chars": 19283,
    "preview": "import inspect\nimport os\nimport shutil\nfrom typing import NamedTuple, NoReturn\nfrom unittest import TestCase, mock\n\nimpo"
  },
  {
    "path": "tests/test_autodelete_photos.py",
    "chars": 41257,
    "preview": "import datetime\nimport glob\nimport inspect\nimport logging\nimport os\nimport shutil\nfrom typing import Any, NoReturn\nfrom "
  },
  {
    "path": "tests/test_cli.py",
    "chars": 20642,
    "preview": "import datetime\nimport inspect\nimport os\nimport shutil\nimport zoneinfo\nfrom argparse import ArgumentError\nfrom typing im"
  },
  {
    "path": "tests/test_download_live_photos.py",
    "chars": 5254,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\n\nimport pytest\n\nfrom icloudpd.base import lp_filename_concatinato"
  },
  {
    "path": "tests/test_download_live_photos_id.py",
    "chars": 4776,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\n\nimport pytest\n\nfrom tests.helpers import (\n    path_from_project"
  },
  {
    "path": "tests/test_download_photos.py",
    "chars": 93326,
    "preview": "import datetime\nimport inspect\nimport logging\nimport os\nimport shutil\nimport sys\nfrom typing import Any, List, NoReturn,"
  },
  {
    "path": "tests/test_download_photos_id.py",
    "chars": 88437,
    "preview": "import datetime\nimport inspect\nimport logging\nimport os\nimport sys\nfrom typing import Any, List, NoReturn, Sequence, Tup"
  },
  {
    "path": "tests/test_download_videos.py",
    "chars": 3087,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\n\nimport pytest\n\nfrom tests.helpers import (\n    path_from_project"
  },
  {
    "path": "tests/test_email_notifications.py",
    "chars": 7916,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\nfrom unittest.mock import patch\n\nimport pytest\nfrom freezegun imp"
  },
  {
    "path": "tests/test_filenames.py",
    "chars": 16084,
    "preview": "from typing import Any, Callable, Dict\nfrom unittest import TestCase\nfrom unittest.mock import Mock\n\nfrom icloudpd.base "
  },
  {
    "path": "tests/test_folder_structure.py",
    "chars": 7807,
    "preview": "import inspect\nimport os\nimport sys\nfrom typing import List, Tuple\nfrom unittest import TestCase\n\nimport pytest\n\nfrom te"
  },
  {
    "path": "tests/test_http.py",
    "chars": 13944,
    "preview": "import json\nimport threading\nimport time\nimport unittest\nfrom http.server import BaseHTTPRequestHandler, HTTPServer\nfrom"
  },
  {
    "path": "tests/test_issue_1220_only_print_filenames_dedup_bug.py",
    "chars": 3717,
    "preview": "\"\"\"Test for issue #1220: --only-print-filenames downloads live photo video files during deduplication\n\nhttps://github.co"
  },
  {
    "path": "tests/test_json_rules.py",
    "chars": 3603,
    "preview": "from unittest import TestCase\n\nfrom foundation.core import constant\nfrom foundation.json import apply_rules, compile_pat"
  },
  {
    "path": "tests/test_keep_icloud_mode.py",
    "chars": 15604,
    "preview": "import datetime\nimport inspect\nimport os\nfrom unittest import TestCase, mock\n\nimport pytest\n\nfrom tests.helpers import p"
  },
  {
    "path": "tests/test_listing_albums.py",
    "chars": 1215,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\n\nimport pytest\n\nfrom tests.helpers import (\n    path_from_project"
  },
  {
    "path": "tests/test_listing_libraries.py",
    "chars": 1901,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase\n\nimport pytest\n\nfrom tests.helpers import (\n    path_from_project"
  },
  {
    "path": "tests/test_listing_recent_photos.py",
    "chars": 7287,
    "preview": "import inspect\nimport json\nimport os\nfrom unittest import TestCase, mock\n\nimport pytest\n\nfrom tests.helpers import (\n   "
  },
  {
    "path": "tests/test_logger.py",
    "chars": 2605,
    "preview": "import datetime\nimport logging\nfrom io import StringIO\nfrom unittest import TestCase\nfrom unittest.mock import MagicMock"
  },
  {
    "path": "tests/test_session_connection_errors.py",
    "chars": 8184,
    "preview": "#!/usr/bin/env python3\n\nimport os\nimport socket\nimport tempfile\nimport threading\nimport time\nimport unittest\nfrom http.s"
  },
  {
    "path": "tests/test_string_helpers.py",
    "chars": 4237,
    "preview": "import datetime\nimport random\nimport string\nimport sys\nfrom unittest import TestCase\n\nimport pytest\n\nfrom icloudpd.strin"
  },
  {
    "path": "tests/test_two_step_auth.py",
    "chars": 7546,
    "preview": "import inspect\nimport os\nfrom unittest import TestCase, mock\n\nimport pytest\n\nfrom pyicloud_ipd.base import PyiCloudServi"
  },
  {
    "path": "tests/test_xmp_sidecar.py",
    "chars": 9042,
    "preview": "from datetime import datetime\nfrom typing import Any, Dict\nfrom unittest import TestCase\n\nfrom foundation import version"
  },
  {
    "path": "tests/vcr_cassettes/2fa_flow_invalid_code.yml",
    "chars": 19611,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/2fa_flow_valid_code.yml",
    "chars": 30609,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/2fa_flow_valid_code_zero_lead.yml",
    "chars": 30609,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/2sa_flow_invalid_code.yml",
    "chars": 19119,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/2sa_flow_valid_code.yml",
    "chars": 27477,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/auth_non_2fa.yml",
    "chars": 14428,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/auth_requires_2fa.yml",
    "chars": 12418,
    "preview": "interactions:\n- request:\n    body: !!python/unicode '{\"accountName\": \"jdoe@gmail.com\", \"protocols\": [\"s2k\", \"s2k_fo\"]}'\n"
  },
  {
    "path": "tests/vcr_cassettes/autodelete_photos.yml",
    "chars": 202468,
    "preview": "interactions:\n- request:\n    body: 'null'\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip, deflate']\n  "
  },
  {
    "path": "tests/vcr_cassettes/autodelete_photos_heic.yml",
    "chars": 202472,
    "preview": "interactions:\n- request:\n    body: 'null'\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip, deflate']\n  "
  },
  {
    "path": "tests/vcr_cassettes/download_autodelete_photos_part1.yml",
    "chars": 1097353,
    "preview": "interactions:\n- request:\n    body: 'null'\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip, deflate']\n  "
  },
  {
    "path": "tests/vcr_cassettes/download_autodelete_photos_part2.yml",
    "chars": 853196,
    "preview": "interactions:\n- request:\n    body: 'null'\n    headers:\n      Accept: ['*/*']\n      Accept-Encoding: ['gzip, deflate']\n  "
  }
]

// ... and 24 more files (download for full content)

About this extraction

This page contains the full source code of the icloud-photos-downloader/icloud_photos_downloader GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 224 files (55.3 MB), approximately 9.3M tokens, and a symbol index with 674 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!