diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c4f20faa..ff83685ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- 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) diff --git a/src/icloudpd/download.py b/src/icloudpd/download.py index f0d13fe8b..b89ae2067 100644 --- a/src/icloudpd/download.py +++ b/src/icloudpd/download.py @@ -35,7 +35,10 @@ def update_mtime(created: datetime.datetime, download_path: str) -> None: def set_utime(download_path: str, created_date: datetime.datetime) -> None: """Set date & time of the file""" - ctime = time.mktime(created_date.timetuple()) + try: + ctime = time.mktime(created_date.timetuple()) + except OverflowError: + ctime = time.mktime(datetime.datetime(1970, 1, 1, 0, 0, 0).timetuple()) os.utime(download_path, (ctime, ctime)) diff --git a/tests/test_download_photos.py b/tests/test_download_photos.py index cb07dbebe..b4ce15071 100644 --- a/tests/test_download_photos.py +++ b/tests/test_download_photos.py @@ -11,6 +11,7 @@ import piexif import pytest +import pytz from click.testing import CliRunner from piexif._exceptions import InvalidImageDataError from requests import Response @@ -942,7 +943,7 @@ def astimezone(self, _tz: (Optional[Any]) = None) -> NoReturn: @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @pytest.mark.skipif(sys.platform == "darwin", reason="does not run on mac") - def test_invalid_creation_year(self) -> None: + def test_creation_date_without_century(self) -> None: base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) files_to_download = [("5/01/01", "IMG_7409.JPG")] @@ -950,11 +951,13 @@ def test_invalid_creation_year(self) -> None: 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: (Optional[Any]) = None) -> NoReturn: - raise ValueError("Invalid date") + # class NewDateTime(datetime.datetime): + # def astimezone(self, _tz: (Optional[Any]) = None) -> NoReturn: + # raise ValueError("Invalid date") - dt_mock.return_value = NewDateTime(5, 1, 1, 0, 0, 0) + dt_mock.return_value = datetime.datetime( + 5, 1, 1, 0, 0, 0, tzinfo=pytz.timezone("America/Los_Angeles") + ) data_dir, result = run_icloudpd_test( self.assertEqual, @@ -985,12 +988,60 @@ def astimezone(self, _tz: (Optional[Any]) = None) -> NoReturn: f"INFO Downloading the first original photo or video to {data_dir} ...", self._caplog.text, ) + # self.assertIn( + # "ERROR Could not convert photo created date to local timezone (0005-01-01 00:00:00)", + # self._caplog.text, + # ) + self.assertIn( + f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('5/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_creation_date_prior_1970(self) -> None: + base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) + + files_to_download = [("1965/01/01", "IMG_7409.JPG")] + + 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: (Optional[Any]) = None) -> NoReturn: + # raise ValueError("Invalid date") + + dt_mock.return_value = datetime.datetime(1965, 1, 1, 0, 0, 0) + + data_dir, result = run_icloudpd_test( + self.assertEqual, + self.root_path, + base_dir, + "listing_photos.yml", + [], + files_to_download, + [ + "--username", + "jdoe@gmail.com", + "--password", + "password1", + "--recent", + "1", + "--skip-live-photos", + "--no-progress-bar", + ], + ) + + self.assertIn( + "DEBUG Looking up all photos and videos...", + self._caplog.text, + ) self.assertIn( - "ERROR Could not convert photo created date to local timezone (0005-01-01 00:00:00)", + f"INFO Downloading the first original photo or video to {data_dir} ...", self._caplog.text, ) self.assertIn( - f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('5/01/01/IMG_7409.JPG'))}", + f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('1965/01/01/IMG_7409.JPG'))}", self._caplog.text, ) self.assertIn("INFO All photos have been downloaded", self._caplog.text) diff --git a/tests/test_download_photos_id.py b/tests/test_download_photos_id.py index 85a0cd26f..ff9b4f854 100644 --- a/tests/test_download_photos_id.py +++ b/tests/test_download_photos_id.py @@ -891,7 +891,7 @@ def astimezone(self, _tz: (Optional[Any]) = None) -> NoReturn: @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") @pytest.mark.skipif(sys.platform == "darwin", reason="does not run on mac") - def test_invalid_creation_year_name_id7(self) -> None: + def test_creation_date_without_century_name_id7(self) -> None: base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) files_to_download = [("5/01/01", "IMG_7409_QVk2Yyt.JPG")] @@ -945,6 +945,56 @@ def astimezone(self, _tz: (Optional[Any]) = None) -> NoReturn: self.assertIn("INFO All photos have been downloaded", self._caplog.text) assert result.exit_code == 0 + def test_creation_date_prior_1970_name_id7(self) -> None: + base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3]) + + files_to_download = [("1965/01/01", "IMG_7409_QVk2Yyt.JPG")] + + 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: (Optional[Any]) = None) -> NoReturn: + # raise ValueError("Invalid date") + + dt_mock.return_value = datetime.datetime(1965, 1, 1, 0, 0, 0) + + data_dir, result = run_icloudpd_test( + self.assertEqual, + self.root_path, + base_dir, + "listing_photos.yml", + [], + files_to_download, + [ + "--username", + "jdoe@gmail.com", + "--password", + "password1", + "--recent", + "1", + "--skip-live-photos", + "--no-progress-bar", + "--file-match-policy", + "name-id7", + ], + ) + + self.assertIn( + "DEBUG Looking up all photos and videos...", + self._caplog.text, + ) + self.assertIn( + f"INFO Downloading the first original photo or video to {data_dir} ...", + self._caplog.text, + ) + self.assertIn( + f"DEBUG Downloading {os.path.join(data_dir, os.path.normpath('1965/01/01/IMG_7409_QVk2Yyt.JPG'))}", + self._caplog.text, + ) + self.assertIn("INFO All photos have been downloaded", self._caplog.text) + assert result.exit_code == 0 + def test_missing_item_type_name_id7(self) -> None: base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])