Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion src/icloudpd/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))


Expand Down
65 changes: 58 additions & 7 deletions tests/test_download_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

import piexif
import pytest
import pytz
from click.testing import CliRunner
from piexif._exceptions import InvalidImageDataError
from requests import Response
Expand Down Expand Up @@ -942,19 +943,21 @@ 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")]

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,
Expand Down Expand Up @@ -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)
Expand Down
52 changes: 51 additions & 1 deletion tests/test_download_photos_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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])

Expand Down
Loading