From 8e53c4895d7355e4cba1b5208a59552bc1e8acec Mon Sep 17 00:00:00 2001 From: Zhiming Wang Date: Sat, 27 Jun 2015 21:20:31 -0700 Subject: 20150627 Automatically clean up "Previous Mobile Applications" --- ...ically-clean-up-previous-mobile-applications.md | 122 +++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 source/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.md (limited to 'source/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.md') diff --git a/source/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.md b/source/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.md new file mode 100644 index 00000000..ff629c3b --- /dev/null +++ b/source/blog/2015-06-27-automatically-clean-up-previous-mobile-applications.md @@ -0,0 +1,122 @@ +--- +title: 'Automatically clean up "Previous Mobile Applications"' +date: 2015-06-27T21:19:59-07:00 +date-display: June 27, 2015 +--- +iTunes keeps a "Previous Mobile Applications" folder of questionable value, which always annoys me. It eats into disk space and wastes syncing/backup cycles and bandwidth; you can easily find horror stories online about [100GB+ PMA folders](http://forums.macrumors.com/threads/5-years-of-deleted-iphone-apps-accumulated-in-my-itunes-library.1781676/#post-19749496). The value? You might be able to roll back to an earlier version, or restore an app pulled from the App Store. Really? I never had that need in my life[^disclosure]; have you? Worst of all, there should be a periodic clean up option — just like how deleted mail are automatically purged after one month, but the option is missing. + +[^disclosure]: Full disclosure: unlike many people, I'm not very obsessed with my phone, and I only have about two dozen third-party apps. + +Therefore, I wrote a trivial Python script to do the periodic cleanup. Feel free to grab my script below (also available at ) to save a few minutes of hacking. It should be plugged into a daily or weekly or monthly cron job (or the equivalent), and it writes data to `~/.local/share/itunes/previous-mobile-applications.json` by default. To customize, just modify the global constants. + +```python +#!/usr/bin/env python3 + +"""Periodically clean up "Previous Mobile Applications" of iTunes.""" + +import arrow +import datetime +import json +import os +import sys + +OFFENDING_DIR = os.path.expanduser("~/Music/iTunes/iTunes Media/Mobile Applications/Previous Mobile Applications") +STORAGE_DIR = os.path.expanduser("~/.local/share/itunes") +STORAGE_FILE = os.path.join(STORAGE_DIR, "previous-mobile-applications.json") + +DELETE_AFTER = datetime.timedelta(days=7) + +def load_storage(): + """Load stored dictionary of seen apps from STORAGE_FILE. + + Returns + ------- + seen_app_dict : dict + Dictionary of (app_filename, first_seen_date) key-value pairs, + where app_filename is str, and last_seen_date is datetime.date. + + """ + os.makedirs(STORAGE_DIR, mode=0o700, exist_ok=True) + try: + with open(STORAGE_FILE, encoding="utf-8") as fp: + serializable_seen_app_dict = json.load(fp) + return {app_filename: arrow.get(serialized_first_seen_date).date() + for app_filename, serialized_first_seen_date in serializable_seen_app_dict.items()} + except OSError: + return {} + +def write_storage(seen_app_dict): + """Write the dictionary of seen apps to STORAGE_FILE. + + Parameters + ---------- + seen_app_dict : dict + See the return format of load_storage(). + + Returns + ------- + 0 or 1 + Return code indicating success or failure. + + """ + # convert datetime.time to str (ISO 8601) + serializable_seen_app_dict = {app_filename: first_seen_date.isoformat() + for app_filename, first_seen_date in seen_app_dict.items()} + os.makedirs(STORAGE_DIR, mode=0o700, exist_ok=True) + try: + with open(STORAGE_FILE, mode="w", encoding="utf-8") as fp: + json.dump(serializable_seen_app_dict, fp, indent=2, sort_keys=True) + return 0 + except OSError as err: + sys.stderr.write("error: failed to write to '%s': %s" % (STORAGE_FILE, str(err))) + return 1 + +def main(): + """Main. + + Returns + ------- + 0 or 1 + Return code indicating success or failure. + + """ + if not os.path.isdir(OFFENDING_DIR): + # good, you don't have that junk + return 0 + + today = datetime.date.today() + seen_app_dict = load_storage() + current_app_list = os.listdir(OFFENDING_DIR) + + # boot already disappeared apps + for app in [app for app in seen_app_dict if app not in current_app_list]: + seen_app_dict.pop(app) + + # add newly appeared apps + for app in [app for app in current_app_list if app not in seen_app_dict]: + seen_app_dict[app] = today + + # delete expired apps + returncode = 0 + newly_deleted_apps = [] + for app in seen_app_dict: + if today >= seen_app_dict[app] + DELETE_AFTER: + app_path = os.path.join(OFFENDING_DIR, app) + try: + os.remove(app_path) + newly_deleted_apps.append(app) + except OSError as err: + sys.stderr.write("error: failed to remove '%s': %s" % (app_path, str(err))) + returncode = 1 + + for app in newly_deleted_apps: + seen_app_dict.pop(app) + + # write data to disk + returncode |= write_storage(seen_app_dict) + + return returncode + +if __name__ == "__main__": + exit(main()) +``` -- cgit v1.2.1