Example #1
0
    def test_listing_albums(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_albums.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--list-albums",
                    "--no-progress-bar",
                ],
            )

            print_result_exception(result)
            albums = result.output.splitlines()

            self.assertIn("All Photos", albums)
            self.assertIn("WhatsApp", albums)
            self.assertIn("Time-lapse", albums)
            self.assertIn("Recently Deleted", albums)
            self.assertIn("Favorites", albums)

            assert result.exit_code == 0
    def test_handle_session_error_during_photo_iteration(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"

            def mock_raise_response_error(offset):
                raise PyiCloudAPIResponseError("Invalid global session", 100)

            with mock.patch("time.sleep") as sleep_mock:
                with mock.patch.object(PhotoAlbum,
                                       "photos_request") as pa_photos_request:
                    pa_photos_request.side_effect = mock_raise_response_error

                    # Let the initial authenticate() call succeed,
                    # but do nothing on the second try.
                    orig_authenticate = PyiCloudService.authenticate

                    def mocked_authenticate(self):
                        if not hasattr(self, "already_authenticated"):
                            orig_authenticate(self)
                            setattr(self, "already_authenticated", True)

                    with mock.patch.object(PyiCloudService,
                                           "authenticate",
                                           new=mocked_authenticate):
                        runner = CliRunner()
                        result = runner.invoke(
                            main,
                            [
                                "--username",
                                "*****@*****.**",
                                "--password",
                                "password1",
                                "--recent",
                                "1",
                                "--skip-videos",
                                "--skip-live-photos",
                                "--no-progress-bar",
                                "-d",
                                "tests/fixtures/Photos",
                            ],
                        )
                        print_result_exception(result)

                        # Error msg should be repeated 5 times
                        assert (self._caplog.text.count(
                            "Session error, re-authenticating...") == 5)

                        self.assertIn(
                            "INFO     iCloud re-authentication failed! Please try again later.",
                            self._caplog.text,
                        )
                        # Make sure we only call sleep 4 times (skip the first retry)
                        self.assertEquals(sleep_mock.call_count, 4)

                        assert result.exit_code == -1
Example #3
0
    def test_listing_recent_photos_with_missing_downloadURL(self):
        base_dir = os.path.normpath("tests/fixtures/Photos")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette(
                "tests/vcr_cassettes/listing_photos_missing_downloadUrl.yml"):
            with mock.patch("icloudpd.base.open", create=True) as mock_open:
                with mock.patch.object(json, "dump") as mock_json:
                    # Pass fixed client ID via environment variable
                    os.environ[
                        "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--only-print-filenames",
                            "--no-progress-bar",
                            "--threads-num",
                            1,
                            "-d",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)

                    self.assertEqual.__self__.maxDiff = None
                    self.assertEqual(
                        """\
KeyError: 'downloadURL' attribute was not found in the photo fields!
icloudpd has saved the photo record to: ./icloudpd-photo-error.json
Please create a Gist with the contents of this file: https://gist.github.com
Then create an issue on GitHub: https://github.com/icloud-photos-downloader/icloud_photos_downloader/issues
Include a link to the Gist in your issue, so that we can see what went wrong.

""", result.output)
                    mock_open.assert_called_once_with(
                        'icloudpd-photo-error.json', 'w')
                    mock_json.assert_called_once()
                    # Check a few keys in the dict
                    first_arg = mock_json.call_args_list[0][0][0]
                    self.assertEqual(first_arg['master_record']['recordName'],
                                     'AY6c+BsE0jjaXx9tmVGJM1D2VcEO')
                    self.assertEqual(
                        first_arg['master_record']['fields']
                        ['resVidSmallHeight']['value'], 581)
                    self.assertEqual(first_arg['asset_record']['recordName'],
                                     'F2A23C38-0020-42FE-A273-2923ADE3CAED')
                    self.assertEqual(
                        first_arg['asset_record']['fields']['assetDate']
                        ['value'], 1533021744816)
                    assert result.exit_code == 0
    def test_missing_size(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch.object(PhotoAsset, "download") as pa_download:
            pa_download.return_value = False

            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "3",
                        "--no-progress-bar",
                        base_dir,
                    ],
                )
                print_result_exception(result)

                self.assertIn(
                    "DEBUG    Looking up all photos and videos...", self._caplog.text
                )
                self.assertIn(
                    "INFO     Downloading 3 original photos and videos to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )

                # These error messages should not be repeated more than once
                assert (
                    self._caplog.text.count(
                        "ERROR    Could not find URL to download IMG_7409.JPG for size original!"
                    )
                    == 1
                )
                assert (
                    self._caplog.text.count(
                        "ERROR    Could not find URL to download IMG_7408.JPG for size original!"
                    )
                    == 1
                )
                assert (
                    self._caplog.text.count(
                        "ERROR    Could not find URL to download IMG_7407.JPG for size original!"
                    )
                    == 1
                )

                self.assertIn(
                    "INFO     All photos have been downloaded!", self._caplog.text
                )
                assert result.exit_code == 0
Example #5
0
    def test_listing_recent_photos(self):
        base_dir = os.path.normpath(f"tests/fixtures/Photos/{inspect.stack()[0][3]}")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            runner = CliRunner(env={
                "CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"
            })
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--only-print-filenames",
                    "--no-progress-bar",
                    "--threads-num",
                    1,
                    "-d",
                    base_dir,
                ],
            )
            print_result_exception(result)
            filenames = result.output.splitlines()

            self.assertEqual(len(filenames), 8)
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/31/IMG_7409.JPG")), filenames[0]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/31/IMG_7409.MOV")), filenames[1]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")), filenames[2]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")), filenames[3]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7407.JPG")), filenames[4]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7407.MOV")), filenames[5]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7405.MOV")), filenames[6]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7404.MOV")), filenames[7]
            )

            assert result.exit_code == 0
    def test_skip_existing_live_photo_print_filenames(self):
        base_dir = os.path.normpath("tests/fixtures/Photos")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # simulate that some of the expected files are there with corret sizes
        os.makedirs(os.path.join(base_dir, "2020/11/04"))
        # one photo and one movie are already there and should be skipped
        # Create dummies with the correct size
        with open(os.path.join(base_dir, "2020/11/04/IMG_0516.HEIC"), "a") as f:
            f.truncate(1651485)
        with open(os.path.join(base_dir, "2020/11/04/IMG_0514_HEVC.MOV"), "a") as f:
            f.truncate(3951774)

        with vcr.use_cassette("tests/vcr_cassettes/download_live_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "3",
                    "--no-progress-bar",
                    "--threads-num",
                    1,
                    "--only-print-filenames",
                    "-d",
                    base_dir,
                ],
            )
            print_result_exception(result)

            filenames = result.output.splitlines()

            print (filenames)

            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2020/11/04/IMG_0514.HEIC")),
                filenames[0]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2020/11/04/IMG_0512.HEIC")),
                filenames[1]
            )
            self.assertEqual(
                os.path.join(base_dir, os.path.normpath("2020/11/04/IMG_0512_HEVC.MOV")),
                filenames[2]
            )

            # Double check that a mocked file does not get listed again. Its already there!
            assert "2020/11/04/IMG_0514_HEVC.MOV" not in filenames

            assert result.exit_code == 0
Example #7
0
    def test_listing_recent_photos_with_missing_filenameEnc(self):
        base_dir = os.path.normpath(f"tests/fixtures/Photos/{inspect.stack()[0][3]}")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos_missing_filenameEnc.yml"):
            with mock.patch("icloudpd.base.open", create=True) as mock_open:
                with mock.patch.object(json, "dump") as mock_json:
                    # Pass fixed client ID via environment variable
                    runner = CliRunner(env={
                        "CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"
                    })
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "5",
                            "--only-print-filenames",
                            "--no-progress-bar",
                            "--threads-num",
                            1,
                            "-d",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)

                    self.assertEqual.__self__.maxDiff = None

                    filenames = result.output.splitlines()

                    # self.assertEqual(len(filenames), 5)
                    self.assertEqual(
                        os.path.join(base_dir, os.path.normpath("2018/07/31/AY6c_BsE0jja.JPG")),
                        filenames[0]
                    )
                    self.assertEqual(
                        os.path.join(base_dir, os.path.normpath("2018/07/31/AY6c_BsE0jja.MOV")),
                        filenames[1]
                    )
                    self.assertEqual(
                        os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7408.JPG")),
                        filenames[2]
                    )
                    self.assertEqual(
                        os.path.join(base_dir, os.path.normpath("2018/07/30/IMG_7408.MOV")),
                        filenames[3]
                    )
                    self.assertEqual(
                        os.path.join(base_dir, os.path.normpath("2018/07/30/AZ_wAGT9P6jh.JPG")),
                        filenames[4]
                    )
                    assert result.exit_code == 0
    def test_handle_connection_error(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"

            def mock_raise_response_error(arg):
                raise ConnectionError("Connection Error")

            with mock.patch.object(PhotoAsset, "download") as pa_download:
                pa_download.side_effect = mock_raise_response_error

                # Let the initial authenticate() call succeed,
                # but do nothing on the second try.
                orig_authenticate = PyiCloudService.authenticate

                def mocked_authenticate(self):
                    if not hasattr(self, "already_authenticated"):
                        orig_authenticate(self)
                        setattr(self, "already_authenticated", True)

                with mock.patch("icloudpd.constants.WAIT_SECONDS", 0):
                    with mock.patch.object(
                        PyiCloudService, "authenticate", new=mocked_authenticate
                    ):
                        runner = CliRunner()
                        result = runner.invoke(
                            main,
                            [
                                "--username",
                                "*****@*****.**",
                                "--password",
                                "password1",
                                "--recent",
                                "1",
                                "--skip-videos",
                                "--skip-live-photos",
                                "--no-progress-bar",
                                "tests/fixtures/Photos",
                            ],
                        )
                        print_result_exception(result)

                        # Error msg should be repeated 5 times
                        assert (
                            self._caplog.text.count(
                                "Error downloading IMG_7409.JPG, retrying after 0 seconds..."
                            )
                            == 5
                        )

                        self.assertIn(
                            "INFO     Could not download IMG_7409.JPG! Please try again later.",
                            self._caplog.text,
                        )
                        assert result.exit_code == 0
    def test_invalid_creation_date(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch.object(PhotoAsset,
                               "created",
                               new_callable=mock.PropertyMock) as dt_mock:
            # Can't mock `astimezone` because it's a readonly property, so have to
            # create a new class that inherits from datetime.datetime
            class NewDateTime(datetime.datetime):
                def astimezone(self, tz=None):
                    raise ValueError('Invalid date')

            dt_mock.return_value = NewDateTime(2018, 1, 1, 0, 0, 0)

            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ[
                    "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-live-photos",
                        "--no-progress-bar",
                        "-d",
                        base_dir,
                    ],
                )
                print_result_exception(result)

                self.assertIn(
                    "DEBUG    Looking up all photos and videos from album All Photos...",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "ERROR    Could not convert photo created date to local timezone (2018-01-01 00:00:00)",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading tests/fixtures/Photos/2018/01/01/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn("INFO     All photos have been downloaded!",
                              self._caplog.text)
                assert result.exit_code == 0
    def test_size_fallback_to_original(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True

            with mock.patch.object(PhotoAsset, "versions") as pa:
                pa.return_value = ["original", "medium"]

                with vcr.use_cassette(
                        "tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ[
                        "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--size",
                            "thumb",
                            "--no-progress-bar",
                            "-d",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)
                    self.assertIn(
                        "DEBUG    Looking up all photos and videos from album All Photos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading the first thumb photo or video to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        self._caplog.text,
                    )
                    self.assertIn("INFO     All photos have been downloaded!",
                                  self._caplog.text)
                    dp_patched.assert_called_once_with(
                        ANY,
                        ANY,
                        "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        "original",
                    )

                    assert result.exit_code == 0
Example #11
0
    def test_folder_structure_none(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--only-print-filenames",
                    "--folder-structure=none",
                    "--no-progress-bar",
                    "-d",
                    "tests/fixtures/Photos",
                ],
            )
            print_result_exception(result)
            filenames = result.output.splitlines()

            self.assertEqual(len(filenames), 8)
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7409.JPG"), filenames[0]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7409.MOV"), filenames[1]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7408.JPG"), filenames[2]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7408.MOV"), filenames[3]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7407.JPG"), filenames[4]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7407.MOV"), filenames[5]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7405.MOV"), filenames[6]
            )
            self.assertEqual(
                normpath("tests/fixtures/Photos/IMG_7404.MOV"), filenames[7]
            )

            assert result.exit_code == 0
    def test_size_fallback_to_original(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True

            with mock.patch.object(PhotoAsset, "versions") as pa:
                pa.return_value = ["original", "medium"]

                with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--size",
                            "thumb",
                            "--no-progress-bar",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)
                    self.assertIn(
                        "DEBUG    Looking up all photos and videos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading the first thumb photo or video to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     All photos have been downloaded!", self._caplog.text
                    )
                    dp_patched.assert_called_once_with(
                        ANY,
                        ANY,
                        "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        "original",
                    )

                    assert result.exit_code == 0
    def test_listing_recent_photos(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--only-print-filenames",
                    "--no-progress-bar",
                    "tests/fixtures/Photos",
                ],
            )
            print_result_exception(result)
            filenames = result.output.splitlines()

            self.assertEqual(len(filenames), 8)
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG", filenames[0]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/31/IMG_7409.MOV", filenames[1]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", filenames[2]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7408.MOV", filenames[3]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7407.JPG", filenames[4]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7407.MOV", filenames[5]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7405.MOV", filenames[6]
            )
            self.assertEqual(
                "tests/fixtures/Photos/2018/07/30/IMG_7404.MOV", filenames[7]
            )


            assert result.exit_code == 0
    def test_invalid_creation_date(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch.object(PhotoAsset, "created", new_callable=mock.PropertyMock) as dt_mock:
            # Can't mock `astimezone` because it's a readonly property, so have to
            # create a new class that inherits from datetime.datetime
            class NewDateTime(datetime.datetime):
                def astimezone(self, tz=None):
                    raise ValueError('Invalid date')
            dt_mock.return_value = NewDateTime(2018,1,1,0,0,0)

            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-live-photos",
                        "--no-progress-bar",
                        base_dir,
                    ],
                )
                print_result_exception(result)

                self.assertIn(
                    "DEBUG    Looking up all photos and videos...",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "ERROR    Could not convert photo created date to local timezone (2018-01-01 00:00:00)",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading tests/fixtures/Photos/2018/01/01/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     All photos have been downloaded!", self._caplog.text
                )
                assert result.exit_code == 0
    def test_download_photos_and_exif_exceptions(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch.object(piexif, "load") as piexif_patched:
            piexif_patched.side_effect = InvalidImageDataError

            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ[
                    "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-videos",
                        "--skip-live-photos",
                        "--set-exif-datetime",
                        "--no-progress-bar",
                        "-d",
                        "tests/fixtures/Photos",
                    ],
                )
                print_result_exception(result)

                self.assertIn(
                    "DEBUG    Looking up all photos from album All Photos...",
                    self._caplog.text)
                self.assertIn(
                    "INFO     Downloading the first original photo to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "DEBUG    Error fetching EXIF data for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "DEBUG    Error setting EXIF data for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn("INFO     All photos have been downloaded!",
                              self._caplog.text)
                assert result.exit_code == 0
    def test_listing_recent_photos_with_missing_downloadURL(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos_missing_downloadUrl.yml"):
            with mock.patch("icloudpd.base.open", create=True) as mock_open:
                with mock.patch.object(json, "dump") as mock_json:
                    # Pass fixed client ID via environment variable
                    os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--only-print-filenames",
                            "--no-progress-bar",
                            "tests/fixtures/Photos",
                        ],
                    )
                    print_result_exception(result)

                    self.assertEqual.__self__.maxDiff = None
                    self.assertEqual("""\
KeyError: 'downloadURL' attribute was not found in the photo fields!
icloudpd has saved the photo record to: ./icloudpd-photo-error.json
Please create a Gist with the contents of this file: https://gist.github.com
Then create an issue on GitHub: https://github.com/ndbroadbent/icloud_photos_downloader/issues
Include a link to the Gist in your issue, so that we can see what went wrong.

""" , result.output)
                    mock_open.assert_called_once_with('icloudpd-photo-error.json', 'w')
                    mock_json.assert_called_once()
                    # Check a few keys in the dict
                    first_arg = mock_json.call_args_list[0][0][0]
                    self.assertEqual(
                        first_arg['master_record']['recordName'],
                        'AY6c+BsE0jjaXx9tmVGJM1D2VcEO')
                    self.assertEqual(
                        first_arg['master_record']['fields']['resVidSmallHeight']['value'],
                        581)
                    self.assertEqual(
                        first_arg['asset_record']['recordName'],
                        'F2A23C38-0020-42FE-A273-2923ADE3CAED')
                    self.assertEqual(
                        first_arg['asset_record']['fields']['assetDate']['value'],
                        1533021744816)
                    assert result.exit_code == 0
Example #17
0
    def test_skip_existing_downloads_sqlite(self, mock_sqlite):
        # all media inqueries return as already processed
        mock_sqlite().processed.return_value = True
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos/2018/07/31/")
        open("tests/fixtures/Photos/2018/07/31/IMG_7409.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/31/IMG_7409.MOV", "a").close()

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "1",
                    # '--skip-videos',
                    # "--skip-live-photos",
                    "--no-progress-bar",
                    "-d",
                    "tests/fixtures/Photos",
                    "--state-store",
                    "sqlite",
                    "--state-path",
                    "tests/fixtures/test.sqlite3"
                ],
            )
            print_result_exception(result)

            self.assertIn(
                "DEBUG    Looking up all photos and videos from album All Photos...",
                self._caplog.text)
            self.assertIn(
                "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/31/IMG_7409.JPG already processed.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/31/IMG_7409.MOV already processed.",
                self._caplog.text,
            )
            self.assertIn("INFO     All photos have been downloaded!",
                          self._caplog.text)
            assert result.exit_code == 0
    def test_unknown_item_type(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True

            with mock.patch.object(PhotoAsset,
                                   "item_type",
                                   new_callable=mock.PropertyMock) as it_mock:
                it_mock.return_value = 'unknown'

                with vcr.use_cassette(
                        "tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ[
                        "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--no-progress-bar",
                            "-d",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)

                    self.assertIn(
                        "DEBUG    Looking up all photos and videos from album All Photos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Skipping IMG_7409.JPG, only downloading photos and videos. (Item type was: unknown)",
                        self._caplog.text,
                    )
                    self.assertIn("INFO     All photos have been downloaded!",
                                  self._caplog.text)
                    dp_patched.assert_not_called

                    assert result.exit_code == 0
    def test_download_photos_and_exif_exceptions(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch.object(piexif, "load") as piexif_patched:
            piexif_patched.side_effect = InvalidImageDataError

            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-videos",
                        "--skip-live-photos",
                        "--set-exif-datetime",
                        "--no-progress-bar",
                        "tests/fixtures/Photos",
                    ],
                )
                print_result_exception(result)

                self.assertIn("DEBUG    Looking up all photos...", self._caplog.text)
                self.assertIn(
                    "INFO     Downloading the first original photo to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "DEBUG    Error fetching EXIF data for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "DEBUG    Error setting EXIF data for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                    self._caplog.text,
                )
                self.assertIn(
                    "INFO     All photos have been downloaded!", self._caplog.text
                )
                assert result.exit_code == 0
    def test_listing_recent_photos_with_missing_filenameEnc(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos_missing_filenameEnc.yml"):
            with mock.patch("icloudpd.base.open", create=True) as mock_open:
                with mock.patch.object(json, "dump") as mock_json:
                    # Pass fixed client ID via environment variable
                    os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "5",
                            "--only-print-filenames",
                            "--no-progress-bar",
                            "tests/fixtures/Photos",
                        ],
                    )
                    print_result_exception(result)

                    self.assertEqual.__self__.maxDiff = None

                    filenames = result.output.splitlines()

                    # self.assertEqual(len(filenames), 5)
                    self.assertEqual(
                        "tests/fixtures/Photos/2018/07/31/AY6c_BsE0jja.JPG", filenames[0]
                    )
                    self.assertEqual(
                        "tests/fixtures/Photos/2018/07/31/AY6c_BsE0jja.MOV", filenames[1]
                    )
                    self.assertEqual(
                        "tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", filenames[2]
                    )
                    self.assertEqual(
                        "tests/fixtures/Photos/2018/07/30/IMG_7408.MOV", filenames[3]
                    )
                    self.assertEqual(
                        "tests/fixtures/Photos/2018/07/30/AZ_wAGT9P6jh.JPG", filenames[4]
                    )
                    assert result.exit_code == 0
    def test_unknown_item_type(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True

            with mock.patch.object(PhotoAsset, "item_type", new_callable=mock.PropertyMock) as it_mock:
                it_mock.return_value = 'unknown'

                with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "1",
                            "--no-progress-bar",
                            base_dir,
                        ],
                    )
                    print_result_exception(result)

                    self.assertIn(
                        "DEBUG    Looking up all photos and videos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Skipping IMG_7409.JPG, only downloading photos and videos. (Item type was: unknown)",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     All photos have been downloaded!", self._caplog.text
                    )
                    dp_patched.assert_not_called

                    assert result.exit_code == 0
    def test_skip_existing_downloads(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos/2018/07/31/")
        open("tests/fixtures/Photos/2018/07/31/IMG_7409.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/31/IMG_7409.MOV", "a").close()

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "1",
                    # '--skip-videos',
                    # "--skip-live-photos",
                    "--no-progress-bar",
                    "tests/fixtures/Photos",
                ],
            )
            print_result_exception(result)

            self.assertIn(
                "DEBUG    Looking up all photos and videos...", self._caplog.text
            )
            self.assertIn(
                "INFO     Downloading the first original photo or video to tests/fixtures/Photos/ ...",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/31/IMG_7409.JPG already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/31/IMG_7409.MOV already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     All photos have been downloaded!", self._caplog.text
            )
            assert result.exit_code == 0
    def test_handle_io_error(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"

            with mock.patch("icloudpd.download.open", create=True) as m:
                # Raise IOError when we try to write to the destination file
                m.side_effect = IOError

                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-videos",
                        "--skip-live-photos",
                        "--no-progress-bar",
                        "-d"
                        "tests/fixtures/Photos",
                    ],
                )
                print_result_exception(result)

                self.assertIn(
                    "DEBUG    Looking up all photos from album All Photos...",
                    self._caplog.text)
                self.assertIn(
                    "INFO     Downloading the first original photo to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "ERROR    IOError while writing file to "
                    "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG! "
                    "You might have run out of disk space, or the file might "
                    "be too large for your OS. Skipping this file...",
                    self._caplog.text,
                )
                assert result.exit_code == 0
Example #24
0
    def test_skip_existing_downloads_for_live_photos(self):
        base_dir = os.path.normpath(
            f"tests/fixtures/Photos/{inspect.stack()[0][3]}")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        with vcr.use_cassette("tests/vcr_cassettes/download_live_photos.yml"):
            # Pass fixed client ID via environment variable
            runner = CliRunner(
                env={"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"})
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "3",
                    "--no-progress-bar",
                    "--threads-num",
                    1,
                    "-d",
                    base_dir,
                ],
            )
            print_result_exception(result)

            self.assertIn(
                f"INFO     Downloading {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0514_HEVC.MOV'))}",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     Downloading {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0514.HEIC'))}",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     Downloading {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0516.HEIC'))}",
                self._caplog.text,
            )
            self.assertIn("INFO     All photos have been downloaded!",
                          self._caplog.text)
            assert result.exit_code == 0
    def test_handle_io_error(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"

            with mock.patch("icloudpd.download.open", create=True) as m:
                # Raise IOError when we try to write to the destination file
                m.side_effect = IOError

                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--recent",
                        "1",
                        "--skip-videos",
                        "--skip-live-photos",
                        "--no-progress-bar",
                        "tests/fixtures/Photos",
                    ],
                )
                print_result_exception(result)

                self.assertIn("DEBUG    Looking up all photos...", self._caplog.text)
                self.assertIn(
                    "INFO     Downloading the first original photo to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )
                self.assertIn(
                    "ERROR    IOError while writing file to "
                    "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG! "
                    "You might have run out of disk space, or the file might "
                    "be too large for your OS. Skipping this file...",
                    self._caplog.text,
                )
                assert result.exit_code == 0
Example #26
0
    def test_listing_photos_does_not_create_folders(self):
        base_dir = os.path.normpath("tests/fixtures/Photos")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # make sure the directory does not exist yet.
        # Should only be created after download, not after just --print-filenames
        self.assertFalse(
            os.path.exists(
                os.path.join(base_dir, os.path.normpath("2018/07/31"))))

        # Note - This test uses the same cassette as test_download_photos.py
        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--only-print-filenames",
                    "--no-progress-bar",
                    "--threads-num",
                    1,
                    "-d",
                    base_dir,
                ],
            )
            print_result_exception(result)
            # make sure the directory still does not exist.
            # Should only be created after download, not after just --print-filenames
            self.assertFalse(
                os.path.exists(
                    os.path.join(base_dir, os.path.normpath("2018/07/31"))))

            assert result.exit_code == 0
Example #27
0
    def test_skip_existing_live_photodownloads(self):
        base_dir = os.path.normpath(
            f"tests/fixtures/Photos/{inspect.stack()[0][3]}")
        if os.path.exists(base_dir):
            shutil.rmtree(base_dir)
        os.makedirs(base_dir)

        # simulate that some of the expected files are there with corret sizes
        os.makedirs(os.path.join(base_dir, "2020/11/04"))
        # one photo and one movie are already there and should be skipped
        # Create dummies with the correct size
        with open(os.path.join(base_dir, "2020/11/04/IMG_0516.HEIC"),
                  "a") as f:
            f.truncate(1651485)
        with open(os.path.join(base_dir, "2020/11/04/IMG_0514_HEVC.MOV"),
                  "a") as f:
            f.truncate(3951774)

        with vcr.use_cassette("tests/vcr_cassettes/download_live_photos.yml"):
            # Pass fixed client ID via environment variable
            runner = CliRunner(
                env={"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"})
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "3",
                    "--no-progress-bar",
                    "--threads-num",
                    1,
                    "-d",
                    base_dir,
                ],
            )
            print_result_exception(result)

            self.assertIn(
                "DEBUG    Looking up all photos and videos from album All Photos...",
                self._caplog.text)
            self.assertIn(
                f"INFO     Downloading 3 original photos and videos to {base_dir} ...",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     Downloading {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0514.HEIC'))}",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0514_HEVC.MOV'))} already exists.",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     Downloading {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0514.HEIC'))}",
                self._caplog.text,
            )
            self.assertIn(
                f"INFO     {os.path.join(base_dir, os.path.normpath('2020/11/04/IMG_0516.HEIC'))} already exists.",
                self._caplog.text,
            )
            self.assertIn("INFO     All photos have been downloaded!",
                          self._caplog.text)
            assert result.exit_code == 0
    def test_download_photos_and_set_exif(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        open("tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/30/IMG_7407.JPG", "a").close()

        # Download the first photo, but mock the video download
        orig_download = PhotoAsset.download

        def mocked_download(self, size):
            if not hasattr(PhotoAsset, "already_downloaded"):
                response = orig_download(self, size)
                setattr(PhotoAsset, "already_downloaded", True)
                return response
            return mock.MagicMock()

        with mock.patch.object(PhotoAsset, "download", new=mocked_download):
            with mock.patch(
                "icloudpd.exif_datetime.get_photo_exif"
            ) as get_exif_patched:
                get_exif_patched.return_value = False
                with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "4",
                            "--set-exif-datetime",
                            # '--skip-videos',
                            # "--skip-live-photos",
                            "--no-progress-bar",
                            "tests/fixtures/Photos",
                        ],
                    )
                    print_result_exception(result)

                    self.assertIn(
                        "DEBUG    Looking up all photos and videos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading 4 original photos and videos to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        self._caplog.text,
                    )
                    # YYYY:MM:DD is the correct format.
                    self.assertIn(
                        "DEBUG    Setting EXIF timestamp for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG: 2018:07:31",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     All photos have been downloaded!", self._caplog.text
                    )
                    assert result.exit_code == 0
    def test_until_found(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        os.makedirs("tests/fixtures/Photos/2018/07/31/")

        files_to_download = []
        files_to_skip = []

        files_to_download.append(("2018/07/31/IMG_7409.JPG", "photo"))
        files_to_download.append(("2018/07/31/IMG_7409-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7408.JPG", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7408-medium.MOV", "photo"))
        files_to_download.append(("2018/07/30/IMG_7407.JPG", "photo"))
        files_to_download.append(("2018/07/30/IMG_7407-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7405.MOV", "video"))
        files_to_skip.append(("2018/07/30/IMG_7404.MOV", "video"))
        files_to_download.append(("2018/07/30/IMG_7403.MOV", "video"))
        files_to_download.append(("2018/07/30/IMG_7402.MOV", "video"))
        files_to_skip.append(("2018/07/30/IMG_7401.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7400.JPG", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7400-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7399.JPG", "photo"))
        files_to_download.append(("2018/07/30/IMG_7399-medium.MOV", "photo"))

        for f in files_to_skip:
            open("%s/%s" % (base_dir, f[0]), "a").close()

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True
            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--live-photo-size",
                        "medium",
                        "--until-found",
                        "3",
                        "--recent",
                        "20",
                        "--no-progress-bar",
                        base_dir,
                    ],
                )
                print_result_exception(result)

                expected_calls = list(
                    map(
                        lambda f: call(
                            ANY, ANY, "%s/%s" % (base_dir, f[0]),
                            "mediumVideo" if (
                                f[1] == 'photo' and f[0].endswith('.MOV')
                            ) else "original"),
                        files_to_download,
                    )
                )
                dp_patched.assert_has_calls(expected_calls)

                self.assertIn(
                    "DEBUG    Looking up all photos and videos...", self._caplog.text
                )
                self.assertIn(
                    "INFO     Downloading ??? original photos and videos to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )

                for f in files_to_skip:
                    expected_message = "INFO     %s/%s already exists." % (base_dir, f[0])
                    self.assertIn(expected_message, self._caplog.text)

                self.assertIn(
                    "INFO     Found 3 consecutive previously downloaded photos. Exiting",
                    self._caplog.text,
                )
                assert result.exit_code == 0
    def test_download_and_skip_existing_photos(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        open("tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/30/IMG_7407.JPG", "a").close()

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--skip-videos",
                    "--skip-live-photos",
                    "--set-exif-datetime",
                    "--no-progress-bar",
                    "tests/fixtures/Photos",
                ],
            )
            print_result_exception(result)

            self.assertIn("DEBUG    Looking up all photos...", self._caplog.text)
            self.assertIn(
                "INFO     Downloading 5 original photos to tests/fixtures/Photos/ ...",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                self._caplog.text,
            )
            self.assertNotIn(
                "IMG_7409.MOV",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/30/IMG_7408.JPG already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/30/IMG_7407.JPG already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Skipping IMG_7405.MOV, only downloading photos.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Skipping IMG_7404.MOV, only downloading photos.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     All photos have been downloaded!", self._caplog.text
            )

            # Check that file was downloaded
            self.assertTrue(
                os.path.exists("tests/fixtures/Photos/2018/07/31/IMG_7409.JPG"))
            # Check that mtime was updated to the photo creation date
            photo_mtime = os.path.getmtime("tests/fixtures/Photos/2018/07/31/IMG_7409.JPG")
            photo_modified_time = datetime.datetime.utcfromtimestamp(photo_mtime)
            self.assertEquals(
                "2018-07-31 07:22:24",
                photo_modified_time.strftime('%Y-%m-%d %H:%M:%S'))

            assert result.exit_code == 0
    def test_download_and_skip_existing_photos(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        open("tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/30/IMG_7407.JPG", "a").close()

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
            runner = CliRunner()
            result = runner.invoke(
                main,
                [
                    "--username",
                    "*****@*****.**",
                    "--password",
                    "password1",
                    "--recent",
                    "5",
                    "--skip-videos",
                    "--skip-live-photos",
                    "--set-exif-datetime",
                    "--no-progress-bar",
                    "-d",
                    "tests/fixtures/Photos",
                ],
            )
            print_result_exception(result)

            self.assertIn(
                "DEBUG    Looking up all photos from album All Photos...",
                self._caplog.text)
            self.assertIn(
                "INFO     Downloading 5 original photos to tests/fixtures/Photos/ ...",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                self._caplog.text,
            )
            self.assertNotIn(
                "IMG_7409.MOV",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/30/IMG_7408.JPG already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     tests/fixtures/Photos/2018/07/30/IMG_7407.JPG already exists.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Skipping IMG_7405.MOV, only downloading photos.",
                self._caplog.text,
            )
            self.assertIn(
                "INFO     Skipping IMG_7404.MOV, only downloading photos.",
                self._caplog.text,
            )
            self.assertIn("INFO     All photos have been downloaded!",
                          self._caplog.text)

            # Check that file was downloaded
            self.assertTrue(
                os.path.exists(
                    "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG"))
            # Check that mtime was updated to the photo creation date
            photo_mtime = os.path.getmtime(
                "tests/fixtures/Photos/2018/07/31/IMG_7409.JPG")
            photo_modified_time = datetime.datetime.utcfromtimestamp(
                photo_mtime)
            self.assertEquals(
                "2018-07-31 07:22:24",
                photo_modified_time.strftime('%Y-%m-%d %H:%M:%S'))

            assert result.exit_code == 0
    def test_until_found(self):
        base_dir = "tests/fixtures/Photos"
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        os.makedirs("tests/fixtures/Photos/2018/07/31/")

        files_to_download = []
        files_to_skip = []

        files_to_download.append(("2018/07/31/IMG_7409.JPG", "photo"))
        files_to_download.append(("2018/07/31/IMG_7409-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7408.JPG", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7408-medium.MOV", "photo"))
        files_to_download.append(("2018/07/30/IMG_7407.JPG", "photo"))
        files_to_download.append(("2018/07/30/IMG_7407-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7405.MOV", "video"))
        files_to_skip.append(("2018/07/30/IMG_7404.MOV", "video"))
        files_to_download.append(("2018/07/30/IMG_7403.MOV", "video"))
        files_to_download.append(("2018/07/30/IMG_7402.MOV", "video"))
        files_to_skip.append(("2018/07/30/IMG_7401.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7400.JPG", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7400-medium.MOV", "photo"))
        files_to_skip.append(("2018/07/30/IMG_7399.JPG", "photo"))
        files_to_download.append(("2018/07/30/IMG_7399-medium.MOV", "photo"))

        for f in files_to_skip:
            open("%s/%s" % (base_dir, f[0]), "a").close()

        with mock.patch("icloudpd.download.download_media") as dp_patched:
            dp_patched.return_value = True
            with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
                # Pass fixed client ID via environment variable
                os.environ[
                    "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                runner = CliRunner()
                result = runner.invoke(
                    main,
                    [
                        "--username",
                        "*****@*****.**",
                        "--password",
                        "password1",
                        "--live-photo-size",
                        "medium",
                        "--until-found",
                        "3",
                        "--recent",
                        "20",
                        "--no-progress-bar",
                        "-d",
                        base_dir,
                    ],
                )
                print_result_exception(result)

                expected_calls = list(
                    map(
                        lambda f: call(
                            ANY, ANY, "%s/%s" % (base_dir, f[0]), "mediumVideo"
                            if (f[1] == 'photo' and f[0].endswith('.MOV')) else
                            "original"),
                        files_to_download,
                    ))
                dp_patched.assert_has_calls(expected_calls)

                self.assertIn(
                    "DEBUG    Looking up all photos and videos from album All Photos...",
                    self._caplog.text)
                self.assertIn(
                    "INFO     Downloading ??? original photos and videos to tests/fixtures/Photos/ ...",
                    self._caplog.text,
                )

                for f in files_to_skip:
                    expected_message = "INFO     %s/%s already exists." % (
                        base_dir, f[0])
                    self.assertIn(expected_message, self._caplog.text)

                self.assertIn(
                    "INFO     Found 3 consecutive previously downloaded photos. Exiting",
                    self._caplog.text,
                )
                assert result.exit_code == 0
    def test_download_photos_and_set_exif(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        os.makedirs("tests/fixtures/Photos/2018/07/30/")
        open("tests/fixtures/Photos/2018/07/30/IMG_7408.JPG", "a").close()
        open("tests/fixtures/Photos/2018/07/30/IMG_7407.JPG", "a").close()

        # Download the first photo, but mock the video download
        orig_download = PhotoAsset.download

        def mocked_download(self, size):
            if not hasattr(PhotoAsset, "already_downloaded"):
                response = orig_download(self, size)
                setattr(PhotoAsset, "already_downloaded", True)
                return response
            return mock.MagicMock()

        with mock.patch.object(PhotoAsset, "download", new=mocked_download):
            with mock.patch("icloudpd.exif_datetime.get_photo_exif"
                            ) as get_exif_patched:
                get_exif_patched.return_value = False
                with vcr.use_cassette(
                        "tests/vcr_cassettes/listing_photos.yml"):
                    # Pass fixed client ID via environment variable
                    os.environ[
                        "CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"
                    runner = CliRunner()
                    result = runner.invoke(
                        main,
                        [
                            "--username",
                            "*****@*****.**",
                            "--password",
                            "password1",
                            "--recent",
                            "4",
                            "--set-exif-datetime",
                            # '--skip-videos',
                            # "--skip-live-photos",
                            "--no-progress-bar",
                            "-d",
                            "tests/fixtures/Photos",
                        ],
                    )
                    print_result_exception(result)

                    self.assertIn(
                        "DEBUG    Looking up all photos and videos from album All Photos...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading 4 original photos and videos to tests/fixtures/Photos/ ...",
                        self._caplog.text,
                    )
                    self.assertIn(
                        "INFO     Downloading tests/fixtures/Photos/2018/07/31/IMG_7409.JPG",
                        self._caplog.text,
                    )
                    # YYYY:MM:DD is the correct format.
                    self.assertIn(
                        "DEBUG    Setting EXIF timestamp for tests/fixtures/Photos/2018/07/31/IMG_7409.JPG: 2018:07:31",
                        self._caplog.text,
                    )
                    self.assertIn("INFO     All photos have been downloaded!",
                                  self._caplog.text)
                    assert result.exit_code == 0
    def test_handle_session_error_during_photo_iteration(self):
        if os.path.exists("tests/fixtures/Photos"):
            shutil.rmtree("tests/fixtures/Photos")
        os.makedirs("tests/fixtures/Photos")

        with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
            # Pass fixed client ID via environment variable
            os.environ["CLIENT_ID"] = "DE309E26-942E-11E8-92F5-14109FE0B321"

            def mock_raise_response_error(offset):
                raise PyiCloudAPIResponseError("Invalid global session", 100)

            with mock.patch("time.sleep") as sleep_mock:
                with mock.patch.object(PhotoAlbum, "photos_request") as pa_photos_request:
                    pa_photos_request.side_effect = mock_raise_response_error

                    # Let the initial authenticate() call succeed,
                    # but do nothing on the second try.
                    orig_authenticate = PyiCloudService.authenticate

                    def mocked_authenticate(self):
                        if not hasattr(self, "already_authenticated"):
                            orig_authenticate(self)
                            setattr(self, "already_authenticated", True)

                    with mock.patch.object(
                        PyiCloudService, "authenticate", new=mocked_authenticate
                    ):
                        runner = CliRunner()
                        result = runner.invoke(
                            main,
                            [
                                "--username",
                                "*****@*****.**",
                                "--password",
                                "password1",
                                "--recent",
                                "1",
                                "--skip-videos",
                                "--skip-live-photos",
                                "--no-progress-bar",
                                "tests/fixtures/Photos",
                            ],
                        )
                        print_result_exception(result)

                        # Error msg should be repeated 5 times
                        assert (
                            self._caplog.text.count(
                                "Session error, re-authenticating..."
                            )
                            == 5
                        )

                        self.assertIn(
                            "INFO     iCloud re-authentication failed! Please try again later.",
                            self._caplog.text,
                        )
                        # Make sure we only call sleep 4 times (skip the first retry)
                        self.assertEquals(sleep_mock.call_count, 4)

                        assert result.exit_code == -1