├── README.md └── download-gb-content.py /README.md: -------------------------------------------------------------------------------- 1 | :bangbang: Notice :bangbang: 2 | === 3 | 4 | An update to GarageBand (v10.1.1) has broke this script. I am no longer maintaining this script at this time. Another project exists here that is maintained: https://github.com/cwindus/garageband 5 | -------------------------------------------------------------------------------- /download-gb-content.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | 4 | # ================================================================================ 5 | # download-gb-content.py 6 | # 7 | # Requires GarageBand 10 to be installed onto the local machine. 8 | # This script downloads the content packages for GarageBand 10. 9 | # 10 | # GarageBand 10 Version: 10.1.0 11 | # 12 | # List package URLs: 13 | # $ ./download-gb-content.py list 14 | # 15 | # Download packages: 16 | # $ ./download-gb-content.py download -o ~/Downloads/GBContent 17 | # 18 | # Based on Hannes Juutilainen's Logic Pro X Content Download Script: 19 | # Hannes Juutilainen 20 | # https://github.com/hjuutilainen/adminscripts 21 | # 22 | # Modified with permission by: 23 | # Erik Gomez 24 | # https://github.com/erikng 25 | # ================================================================================ 26 | 27 | import sys 28 | import subprocess 29 | import os 30 | import re 31 | import plistlib 32 | import urllib2 33 | import shutil 34 | import argparse 35 | from urllib2 import HTTPError 36 | 37 | base_url = "file:///Applications/GarageBand.app/Contents/Resources/" 38 | web_url_legacy = "http://audiocontentdownload.apple.com/lp10_ms3_content_2013/" 39 | web_url = "http://audiocontentdownload.apple.com/lp10_ms3_content_2015/" 40 | version = "1010" # For 10.1.0 41 | gb_plist_name = "garageband%s.plist" % version 42 | 43 | def human_readable_size(bytes): 44 | """ 45 | Converts bytes to human readable string 46 | """ 47 | for x in ['bytes','KB','MB','GB']: 48 | if bytes < 1024.0: 49 | return "%3.1f %s" % (bytes, x) 50 | bytes /= 1000.0 # This seems to be the Apple default 51 | return "%3.1f %s" % (bytes, 'TB') 52 | 53 | 54 | def download_package_as(url, output_file): 55 | """ 56 | Downloads an URL to the specified file path 57 | """ 58 | if not url or not output_file: 59 | return False 60 | 61 | try: 62 | req = urllib2.urlopen(url) 63 | with open(output_file, 'wb') as fp: 64 | shutil.copyfileobj(req, fp) 65 | except HTTPError, e: 66 | print "HTTP Error:", e.code, url 67 | 68 | return True 69 | 70 | 71 | def download_gb_plist(): 72 | """ 73 | Downloads the GarageBand Content property list and 74 | returns a dictionary 75 | """ 76 | plist_url = ''.join([base_url, gb_plist_name]) 77 | try: 78 | f = urllib2.urlopen(plist_url) 79 | plist_data = f.read() 80 | f.close() 81 | except BaseException as e: 82 | raise ProcessorError("Can't download %s: %s" % (base_url, e)) 83 | 84 | info_plist = plistlib.readPlistFromString(plist_data) 85 | return info_plist 86 | 87 | 88 | def process_package_download(download_url, save_path, download_size): 89 | """ 90 | Downloads the URL if it doesn't already exist 91 | """ 92 | download_size_string = human_readable_size(download_size) 93 | site = urllib2.urlopen(download_url) 94 | meta = site.info() 95 | if os.path.exists(save_path): 96 | # Check the local file size and download if it's smaller. 97 | # TODO: Get a better way for this. The 'DownloadSize' key in gb_plist 98 | # seems to be wrong for a number of packages. 99 | if int(os.path.getsize(save_path)) < int(meta.getheaders("Content-Length")[0]): 100 | print "Local file size is %s. Remote file is larger. Downloading %s bytes from %s" % (os.path.getsize(save_path), meta.getheaders("Content-Length")[0], download_url) 101 | download_package_as(download_url, save_path) 102 | else: 103 | print "Skipping already downloaded package %s" % download_url 104 | else: 105 | print "Downloading %s bytes from %s" % (meta.getheaders("Content-Length")[0], download_url) 106 | download_package_as(download_url, save_path) 107 | 108 | pass 109 | 110 | 111 | def process_content_item(content_item, parent_items, list_only=False): 112 | """ 113 | Extracts and processes information from a single Content item 114 | """ 115 | # Get the _LOCALIZABLE_ key which contains the human readable name 116 | localizable_items = content_item.get('_LOCALIZABLE_', []) 117 | display_name = localizable_items[0].get('DisplayName') 118 | new_parent_items = None 119 | 120 | # Check if this item is a child of another Content item 121 | if parent_items: 122 | display_names = [] 123 | for parent_item in parent_items: 124 | localizable_parent_items = parent_item.get('_LOCALIZABLE_', []) 125 | parent_display_name = localizable_parent_items[0].get('DisplayName') 126 | display_names.append(parent_display_name) 127 | display_names.append(display_name) 128 | display_names.insert(0, download_directory) 129 | relative_path = os.path.join(*display_names) 130 | new_parent_items = list(parent_items) 131 | new_parent_items.append(content_item) 132 | else: 133 | relative_path = os.path.join(download_directory, display_name) 134 | new_parent_items = list([content_item]) 135 | 136 | # Check if this item contains child Content items and process them 137 | subcontent = content_item.get('SubContent', None) 138 | if subcontent: 139 | for subcontent_item in subcontent: 140 | process_content_item(subcontent_item, new_parent_items, list_only) 141 | 142 | # We don't have any subcontent so get the package references and download 143 | else: 144 | package_name = content_item.get('Packages', None) 145 | if not os.path.exists(relative_path) and not list_only: 146 | #print "Creating dir %s" % relative_path 147 | os.makedirs(relative_path) 148 | 149 | if isinstance(package_name, str): 150 | package_dict = packages.get(package_name, {}) 151 | download_name = package_dict.get('DownloadName', None) 152 | download_size = package_dict.get('DownloadSize', None) 153 | save_path = "".join([relative_path, '/', download_name]) 154 | if "../lp10_ms3_content_2013/" in save_path: 155 | save_path = save_path.replace("../lp10_ms3_content_2013/", "") 156 | if "../lp10_ms3_content_2013/" not in download_name: 157 | download_url = ''.join([web_url, download_name]) 158 | else: 159 | download_name = download_name.replace("../lp10_ms3_content_2013/", "") 160 | download_url = ''.join([web_url_legacy, download_name]) 161 | site = urllib2.urlopen(download_url) 162 | meta = site.info() 163 | if list_only: 164 | print download_url, 'File size: ', meta.getheaders("Content-Length")[0] 165 | else: 166 | process_package_download(download_url, save_path, download_size) 167 | 168 | if isinstance(package_name, list): 169 | for i in package_name: 170 | package_dict = packages.get(i, {}) 171 | download_name = package_dict.get('DownloadName', None) 172 | if "../lp10_ms3_content_2013/" not in download_name: 173 | download_name = download_name.replace("../lp10_ms3_content_2013/", "") 174 | download_size = package_dict.get('DownloadSize', None) 175 | save_path = "".join([relative_path, '/', download_name]) 176 | download_url = ''.join([base_url, download_name]) 177 | site = urllib2.urlopen(download_url) 178 | meta = site.info() 179 | if list_only: 180 | print download_url, 'File size: ', meta.getheaders("Content-Length")[0] 181 | else: 182 | process_package_download(download_url, save_path, download_size) 183 | 184 | def main(argv=None): 185 | # ================ 186 | # Arguments 187 | # ================ 188 | 189 | # Create the top-level parser 190 | parser = argparse.ArgumentParser() 191 | subparsers = parser.add_subparsers(title='subcommands', dest='subparser_name') 192 | 193 | # List 194 | parser_install = subparsers.add_parser('list', help='List package URLs') 195 | 196 | # Download 197 | parser_activate = subparsers.add_parser('download', help='Download packages') 198 | parser_activate.add_argument('-o', '--output', nargs=1, required=True, help='Download location. For example ~/Downloads/GBContent') 199 | 200 | # Parse arguments 201 | args = vars(parser.parse_args()) 202 | 203 | # ================================================================= 204 | # Download the property list which contains the package references 205 | # ================================================================= 206 | gb_plist = download_gb_plist() 207 | 208 | global download_directory 209 | if args.get('output', None): 210 | download_directory = os.path.abspath(args['output'][0]) 211 | else: 212 | home = os.path.expanduser('~') 213 | download_directory = os.path.join(home, 'Downloads/GBContent') 214 | 215 | # ===================================== 216 | # Parse the property list for packages 217 | # ===================================== 218 | global packages 219 | packages = gb_plist['Packages'] 220 | content = gb_plist['Content'] 221 | for content_item in content: 222 | if args['subparser_name'] == 'list': 223 | process_content_item(content_item, None, list_only=True) 224 | else: 225 | process_content_item(content_item, None, list_only=False) 226 | 227 | return 0 228 | 229 | if __name__ == '__main__': 230 | sys.exit(main()) 231 | --------------------------------------------------------------------------------