Skip to content

Commit 08ed88a

Browse files
authored
Delete photo/video after download successful (#431)
* --delete-on-download option
1 parent d9d3c14 commit 08ed88a

File tree

7 files changed

+148
-2
lines changed

7 files changed

+148
-2
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,10 @@ Options:
108108
there is no tty attached)
109109
--threads-num INTEGER RANGE Number of cpu threads -- deprecated. To be
110110
removed in future version
111+
--delete-after-download Delete the photo/video after download it.
112+
The deleted items will be appear in the
113+
"Recently Deleted". Therefore, should not
114+
combine with --auto-delete option.
111115
--version Show the version and exit.
112116
-h, --help Show this message and exit.
113117
```

icloudpd/base.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import itertools
1010
import subprocess
1111
import json
12+
import urllib
1213
import click
1314

1415
from tqdm import tqdm
@@ -193,6 +194,13 @@
193194
type=click.IntRange(1),
194195
default=1,
195196
)
197+
@click.option(
198+
"--delete-after-download",
199+
help='Delete the photo/video after download it.'
200+
+ ' The deleted items will be appear in the "Recently Deleted".'
201+
+ ' Therefore, should not combine with --auto-delete option.',
202+
is_flag=True,
203+
)
196204
@click.version_option()
197205
# pylint: disable-msg=too-many-arguments,too-many-statements
198206
# pylint: disable-msg=too-many-branches,too-many-locals
@@ -224,6 +232,7 @@ def main(
224232
no_progress_bar,
225233
notification_script,
226234
threads_num, # pylint: disable=W0613
235+
delete_after_download
227236
):
228237
"""Download all iCloud photos to a local directory"""
229238

@@ -246,6 +255,10 @@ def main(
246255
print('--directory or --list-albums are required')
247256
sys.exit(2)
248257

258+
if auto_delete and delete_after_download:
259+
print('--auto-delete and --delete-after-download are mutually exclusive')
260+
sys.exit(2)
261+
249262
raise_error_on_2sa = (
250263
smtp_username is not None
251264
or notification_email is not None
@@ -541,6 +554,32 @@ def download_photo(counter, photo):
541554
icloud, photo, lp_download_path, lp_size
542555
)
543556

557+
def delete_photo(photo):
558+
"""Delete a photo from the iCloud account."""
559+
logger.info("Deleting %s", photo.filename)
560+
# pylint: disable=W0212
561+
url = f"{icloud.photos._service_endpoint}/records/modify?"\
562+
f"{urllib.parse.urlencode(icloud.photos.params)}"
563+
post_data = json.dumps(
564+
{
565+
"atomic": True,
566+
"desiredKeys": ["isDeleted"],
567+
"operations": [{
568+
"operationType": "update",
569+
"record": {
570+
"fields": {'isDeleted': {'value': 1}},
571+
"recordChangeTag": photo._asset_record["recordChangeTag"],
572+
"recordName": photo._asset_record["recordName"],
573+
"recordType": "CPLAsset",
574+
}
575+
}],
576+
"zoneID": {"zoneName": "PrimarySync"}
577+
}
578+
)
579+
icloud.photos.session.post(
580+
url, data=post_data, headers={
581+
"Content-type": "application/json"})
582+
544583
consecutive_files_found = Counter(0)
545584

546585
def should_break(counter):
@@ -557,6 +596,8 @@ def should_break(counter):
557596
break
558597
item = next(photos_iterator)
559598
download_photo(consecutive_files_found, item)
599+
if delete_after_download:
600+
delete_photo(item)
560601
except StopIteration:
561602
break
562603

icloudpd/download.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,11 @@ def update_mtime(photo, download_path):
2727
return
2828
set_utime(download_path, created_date)
2929

30-
3130
def set_utime(download_path, created_date):
3231
"""Set date & time of the file"""
3332
ctime = time.mktime(created_date.timetuple())
3433
os.utime(download_path, (ctime, ctime))
3534

36-
3735
def download_media(icloud, photo, download_path, size):
3836
"""Download the photo to path, with retries and error handling"""
3937
logger = setup_logger()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ python_dateutil==2.8.2
55
requests==2.28.2
66
tqdm==4.64.1
77
piexif==1.1.3
8+
urllib3==1.26.6

tests/test_cli.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,20 @@ def test_missing_directory_param(self):
151151
],
152152
)
153153
assert result.exit_code == 2
154+
155+
def test_conflict_options_delete_after_download_and_auto_delete(self):
156+
runner = CliRunner()
157+
result = runner.invoke(
158+
main,
159+
[
160+
"--username",
161+
"jdoe@gmail.com",
162+
"--password",
163+
"password1",
164+
"-d",
165+
"/tmp",
166+
"--delete-after-download",
167+
"--auto-delete"
168+
],
169+
)
170+
assert result.exit_code == 2

tests/test_download_photos.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1295,3 +1295,58 @@ def test_download_chinese(self):
12951295
photo_modified_time.strftime('%Y-%m-%d %H:%M:%S'))
12961296

12971297
assert result.exit_code == 0
1298+
1299+
def test_download_after_delete(self):
1300+
base_dir = os.path.normpath(f"tests/fixtures/Photos/{inspect.stack()[0][3]}")
1301+
if os.path.exists(base_dir):
1302+
shutil.rmtree(base_dir)
1303+
os.makedirs(base_dir)
1304+
1305+
with mock.patch.object(piexif, "insert") as piexif_patched:
1306+
piexif_patched.side_effect = InvalidImageDataError
1307+
with mock.patch(
1308+
"icloudpd.exif_datetime.get_photo_exif"
1309+
) as get_exif_patched:
1310+
get_exif_patched.return_value = False
1311+
with vcr.use_cassette("tests/vcr_cassettes/listing_photos.yml"):
1312+
# Pass fixed client ID via environment variable
1313+
runner = CliRunner(env={
1314+
"CLIENT_ID": "DE309E26-942E-11E8-92F5-14109FE0B321"
1315+
})
1316+
result = runner.invoke(
1317+
main,
1318+
[
1319+
"--username",
1320+
"jdoe@gmail.com",
1321+
"--password",
1322+
"password1",
1323+
"--recent",
1324+
"1",
1325+
"--skip-videos",
1326+
"--skip-live-photos",
1327+
"--no-progress-bar",
1328+
"--threads-num",
1329+
1,
1330+
"--delete-after-download",
1331+
"-d",
1332+
base_dir,
1333+
],
1334+
)
1335+
print_result_exception(result)
1336+
1337+
self.assertIn("DEBUG Looking up all photos from album All Photos...", self._caplog.text)
1338+
self.assertIn(
1339+
f"INFO Downloading the first original photo to {base_dir} ...",
1340+
self._caplog.text,
1341+
)
1342+
self.assertIn(
1343+
f"INFO Downloading {os.path.join(base_dir, os.path.normpath('2018/07/31/IMG_7409.JPG'))}",
1344+
self._caplog.text,
1345+
)
1346+
self.assertIn(
1347+
"INFO Deleting IMG_7409.JPG", self._caplog.text
1348+
)
1349+
self.assertIn(
1350+
"INFO All photos have been downloaded!", self._caplog.text
1351+
)
1352+
assert result.exit_code == 0

tests/vcr_cassettes/listing_photos.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44551,4 +44551,34 @@ interactions:
4455144551
apple-seq: ['0']
4455244552
apple-tk: ['false']
4455344553
status: {code: 200, message: OK}
44554+
- request:
44555+
body: '{"atomic": true, "desiredKeys": ["isDeleted"], "operations": [{"operationType":
44556+
"update", "record": {"fields": {"isDeleted": {"value": 1}}, "recordChangeTag":
44557+
"49lh", "recordName": "F2A23C38-0020-42FE-A273-2923ADE3CAED", "recordType":
44558+
"CPLAsset"}}], "zoneID": {"zoneName": "PrimarySync"}}'
44559+
headers:
44560+
Accept:
44561+
- '*/*'
44562+
Accept-Encoding:
44563+
- gzip, deflate
44564+
Connection:
44565+
- keep-alive
44566+
Content-Length:
44567+
- '288'
44568+
Content-type:
44569+
- application/json
44570+
Origin:
44571+
- https://www.icloud.com
44572+
Referer:
44573+
- https://www.icloud.com/
44574+
User-Agent:
44575+
- Opera/9.52 (X11; Linux i686; U; en)
44576+
method: POST
44577+
uri: https://p10-ckdatabasews.icloud.com/database/1/com.apple.photos.cloud/production/private/records/modify?clientBuildNumber=17DHotfix5&clientMasteringNumber=17DHotfix5&ckjsBuildVersion=17DProjectDev77&ckjsVersion=2.0.5&clientId=DE309E26-942E-11E8-92F5-14109FE0B321&dsid=185776146&remapEnums=True&getCurrentSyncToken=True
44578+
response:
44579+
body:
44580+
string: "{}"
44581+
headers:
44582+
Content-type: application/json
44583+
status: {code: 200, message: OK}
4455444584
version: 1

0 commit comments

Comments
 (0)