Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add command line arguments to filter by date. #779

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ There are three ways to run `icloudpd`:
- One time download and an option to monitor for iCloud changes continuously (`--watch-with-interval` option)
- Optimizations for incremental runs (`--until-found` and `--recent` options)
- Photo meta data (EXIF) updates (`--set-exif-datetime` option)
- Filter items to download by creation date (`--date-before` and/or `--date-after`)
- ... and many more (use `--help` option to get full list)

## Experimental Mode
Expand Down
48 changes: 48 additions & 0 deletions src/icloudpd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,16 @@
type=click.Choice(["com", "cn"]),
default="com",
)
@click.option("--date-before",
itwasabhi marked this conversation as resolved.
Show resolved Hide resolved
help="Latest date with which to filter which photo/videos will be downloaded, "
"specified in the format YYYY-MM-DD.",
default=None,
)
@click.option("--date-after",
help="Earliest date with which to filter which photo/videos will be downloaded, "
"specified in the format YYYY-MM-DD.",
default=None,
)
@click.option("--watch-with-interval",
help="Run downloading in a infinite cycle, waiting specified seconds between runs",
type=click.IntRange(1),
Expand Down Expand Up @@ -281,6 +291,8 @@ def main(
threads_num: int, # pylint: disable=W0613
delete_after_download: bool,
domain: str,
date_before: str,
date_after: str,
watch_with_interval: Optional[int],
dry_run: bool
):
Expand Down Expand Up @@ -324,6 +336,25 @@ def main(
)
sys.exit(2)

if date_before:
try:
date_before = datetime.datetime.strptime(
date_before, "%Y-%m-%d")
except ValueError:
print("Given --date-before does not match required format YYYY-MM-DD.")
sys.exit(2)

date_before = date_before.replace(tzinfo=get_localzone())

if date_after:
try:
date_after = datetime.datetime.strptime(date_after, "%Y-%m-%d")
except ValueError:
print("Given --date-after does not match required format YYYY-MM-DD.")
sys.exit(2)

date_after = date_after.replace(tzinfo=get_localzone())

sys.exit(
core(
download_builder(
Expand All @@ -337,6 +368,8 @@ def main(
set_exif_datetime,
skip_live_photos,
live_photo_size,
date_before,
date_after,
dry_run) if directory is not None else (
lambda _s: lambda _c,
_p: False),
Expand Down Expand Up @@ -387,13 +420,16 @@ def download_builder(
set_exif_datetime: bool,
skip_live_photos: bool,
live_photo_size: str,
date_before: datetime.datetime,
date_after: datetime.datetime,
dry_run: bool) -> Callable[[PyiCloudService], Callable[[Counter, PhotoAsset], bool]]:
"""factory for downloader"""
def state_(
icloud: PyiCloudService) -> Callable[[Counter, PhotoAsset], bool]:
def download_photo_(counter: Counter, photo: PhotoAsset) -> bool:
"""internal function for actually downloading the photos"""
filename = clean_filename(photo.filename)

if skip_videos and photo.item_type != "image":
logger.debug(
"Skipping %s, only downloading photos." +
Expand All @@ -418,6 +454,18 @@ def download_photo_(counter: Counter, photo: PhotoAsset) -> bool:
photo.created)
created_date = photo.created

if date_before and created_date > date_before:
logger.debug(
"Skipping %s, date is after the given latest date.",
filename)
return False

if date_after and created_date < date_after:
logger.debug(
"Skipping %s, date is before the given earliest date.",
filename)
return False

try:
if folder_structure.lower() == "none":
date_path = ""
Expand Down
34 changes: 34 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,37 @@ def test_conflict_options_delete_after_download_and_auto_delete(self):
],
)
assert result.exit_code == 2

def test_bad_date_before_format(self):
runner = CliRunner()
result = runner.invoke(
main,
[
"--username",
"[email protected]",
"--password",
"password1",
"-d",
"/tmp",
"--date-before",
"alpha"
],
)
assert result.exit_code == 2

def test_bad_date_after_format(self):
runner = CliRunner()
result = runner.invoke(
main,
[
"--username",
"[email protected]",
"--password",
"password1",
"-d",
"/tmp",
"--date-after",
"alpha"
],
)
assert result.exit_code == 2
88 changes: 88 additions & 0 deletions tests/test_download_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -2293,3 +2293,91 @@ def raise_response_error(a0_, a1_, a2_):

self.assertEqual(sum(1 for _ in files_in_result),
0, "Files in the result")

def test_skip_by_date_before(self):
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
cookie_dir = os.path.join(base_dir, "cookie")
data_dir = os.path.join(base_dir, "data")

for dir in [base_dir, cookie_dir, data_dir]:
recreate_path(dir)

default_args = [
"--username",
"[email protected]",
"--password",
"password1",
"--recent",
"1",
"--skip-live-photos",
"--skip-videos",
"--dry-run",
"--no-progress-bar",
"--threads-num",
1,
"-d",
data_dir,
"--cookie-directory",
cookie_dir,
]

with vcr.use_cassette(os.path.join(self.vcr_path, "listing_photos.yml")):
runner = CliRunner(env={
"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"
})

result = runner.invoke(
main,
default_args + ["--date-before", "2000-01-01"],
itwasabhi marked this conversation as resolved.
Show resolved Hide resolved
)
print_result_exception(result)

assert result.exit_code == 0
self.assertIn(
"DEBUG Looking up all photos from album All Photos...", self._caplog.text)
self.assertIn(
f"DEBUG Skipping IMG_7409.JPG, date is after", self._caplog.text
itwasabhi marked this conversation as resolved.
Show resolved Hide resolved
)

def test_skip_by_date_after(self):
base_dir = os.path.join(self.fixtures_path, inspect.stack()[0][3])
cookie_dir = os.path.join(base_dir, "cookie")
data_dir = os.path.join(base_dir, "data")

for dir in [base_dir, cookie_dir, data_dir]:
recreate_path(dir)

default_args = [
"--username",
"[email protected]",
"--password",
"password1",
"--recent",
"1",
"--skip-live-photos",
"--skip-videos",
"--dry-run",
"--no-progress-bar",
"--threads-num",
1,
"-d",
data_dir,
"--cookie-directory",
cookie_dir,
]
with vcr.use_cassette(os.path.join(self.vcr_path, "listing_photos.yml")):
runner = CliRunner(env={
"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"
})

result = runner.invoke(
main,
default_args + ["--date-after", "2020-01-01"],
)
print_result_exception(result)
assert result.exit_code == 0
self.assertIn(
"DEBUG Looking up all photos from album All Photos...", self._caplog.text)
self.assertIn(
f"DEBUG Skipping IMG_7409.JPG, date is before", self._caplog.text
)
Loading