[READ-ONLY] Mirror of https://github.com/mrgnw/scripts.
0

Configure Feed

Select the types of activity you want to include in your feed.

scripts / wallget.py
5.8 kB 209 lines
1# https://github.com/mikeswanson/wallget/blob/main/wallget.py 2 3import http.client 4import json 5import os 6import plistlib 7import shutil 8import ssl 9import time 10import urllib.parse 11from multiprocessing.pool import ThreadPool 12from typing import Tuple 13 14IDLEASSETSD_PATH = '/Library/Application Support/com.apple.idleassetsd' 15STRINGS_PATH = f'{IDLEASSETSD_PATH}/Customer/TVIdleScreenStrings.bundle/en.lproj/Localizable.nocache.strings' 16ENTRIES_PATH = f'{IDLEASSETSD_PATH}/Customer/entries.json' 17VIDEO_PATH = f'{IDLEASSETSD_PATH}/Customer/4KSDR240FPS' 18 19 20def main(): 21 # Check if running as admin 22 if os.geteuid() != 0: 23 print(f'Please run as admin: sudo python3 "{__file__}"') 24 exit() 25 26 print('WallGet Live Wallpaper Download/Delete Script') 27 print('---------------------------------------------\n') 28 29 # Validate environment 30 if not os.path.isdir(IDLEASSETSD_PATH): 31 print('Unable to find idleassetsd path.') 32 exit() 33 if not os.path.isfile(STRINGS_PATH): 34 print('Unable to find localizable strings file.') 35 exit() 36 if not os.path.isfile(ENTRIES_PATH): 37 print('Unable to find entries.json file.') 38 exit() 39 if not os.path.isdir(VIDEO_PATH): 40 print('Unable to find video path.') 41 exit() 42 43 # Read localizable strings 44 with open(STRINGS_PATH, 'rb') as fp: 45 strings = plistlib.load(fp) 46 47 # Read asset entries 48 asset_entries = json.load(open(ENTRIES_PATH)) 49 50 # Show categories 51 item = 0 52 categories = asset_entries.get('categories', []) 53 for category in categories: 54 name = strings.get(category.get('localizedNameKey', ''), '') 55 item += 1 56 print(f'{item}. {name}') 57 print(f'{item + 1}. All') 58 59 # Select category 60 category_index = as_int(input('\nCategory number? ')) 61 if category_index < 1 or category_index > item + 1: 62 print('\nNo category selected.') 63 exit() 64 category_id = categories[int(category_index) - 1]['id'] if category_index <= item else None 65 66 # Download or delete? 67 action = input('\n(d)Download or (x)delete? (d/x) ').strip().lower() 68 if action != 'd' and action != 'x': 69 print('\nNo action selected.') 70 exit() 71 action_text = 'download' if action == 'd' else 'delete' 72 73 # Determine items 74 print(f'\nDetermining {action_text} size...', end='') 75 items = [] 76 total_bytes = 0 77 for asset in asset_entries.get('assets', []): 78 if category_id and category_id not in asset.get('categories', []): 79 continue 80 81 label = strings.get(asset.get('localizedNameKey', ''), '') 82 id = asset.get('id', '') 83 84 # NOTE: May need to update this key logic if other formats are added 85 url = asset.get('url-4K-SDR-240FPS', '') 86 87 # Valid asset? 88 if not label or not id or not url: 89 continue 90 91 path = urllib.parse.urlparse(url).path 92 ext = os.path.splitext(path)[1] 93 file_path = f'{VIDEO_PATH}/{id}{ext}' 94 95 # Download if file doesn't exist or is the wrong size 96 file_exists = os.path.isfile(file_path) 97 file_size = os.path.getsize(file_path) if file_exists else 0 98 if action == 'd': 99 content_length = get_content_length(url) 100 print('.', end='', flush=True) 101 if not file_exists or file_size != content_length: 102 items.append((label, url, file_path)) 103 total_bytes += content_length 104 elif action == 'x' and file_exists: 105 items.append((label, url, file_path)) 106 total_bytes += file_size 107 108 print('done.\n') 109 110 # Anything to process? 111 if not items: 112 print(f'Nothing to {action_text}.') 113 exit() 114 115 # Disk space check 116 free_space = shutil.disk_usage('/').free 117 print(f'Available space: {format_bytes(free_space)}') 118 print(f'Files to {action_text} ({len(items)}): {format_bytes(total_bytes)}') 119 if action == 'd' and total_bytes > free_space: 120 print('Not enough disk space to download all files.') 121 exit() 122 123 proceed = input(f'{action_text.capitalize()} files? (y/n) ').strip().lower() 124 if proceed != 'y': 125 exit() 126 127 if action == 'd': 128 start_time = time.time() 129 print('\nDownloading...') 130 results = ThreadPool().imap_unordered(download_file, items) 131 for result in results: 132 print(f" Downloaded '{result}'") 133 print(f'\nDownloaded {len(items)} files in {time.time() - start_time:.1f}s.') 134 elif action == 'x': 135 print('\nDeleting...') 136 for item in items: 137 label, _, file_path = item 138 os.remove(file_path) 139 print(f" Deleted '{label}'") 140 print(f'\nDeleted {len(items)} files.') 141 142 # Optionally kill idleassetsd to update wallpaper status 143 should_kill = input('\nKill idleassetsd to update download status in Settings? (y/n) ').strip().lower() 144 if should_kill == 'y': 145 os.system('killall idleassetsd') 146 print('Killed idleassetsd.') 147 148 print('\nDone.') 149 150 151def as_int(s: str) -> int: 152 try: 153 return int(s) 154 except ValueError: 155 return -1 156 157 158def format_bytes(bytes: int) -> str: 159 units = ( 160 (1 << 50, 'PB'), 161 (1 << 40, 'TB'), 162 (1 << 30, 'GB'), 163 (1 << 20, 'MB'), 164 (1 << 10, 'KB'), 165 (1, 'bytes'), 166 ) 167 if bytes == 1: 168 return '1 byte' 169 for factor, suffix in units: 170 if bytes >= factor: 171 break 172 return f'{bytes / factor:.2f} {suffix}' 173 174 175def connect(parsed_url: urllib.parse.ParseResult) -> http.client.HTTPConnection: 176 context = ssl._create_unverified_context() 177 conn = ( 178 http.client.HTTPSConnection(parsed_url.netloc, context=context) 179 if parsed_url.scheme == 'https' 180 else http.client.HTTPConnection(parsed_url.netloc) 181 ) 182 return conn 183 184 185def get_content_length(url: str) -> int: 186 parsed_url = urllib.parse.urlparse(url) 187 conn = connect(parsed_url) 188 conn.request('HEAD', parsed_url.path) 189 r = conn.getresponse() 190 content_length = int(r.getheader('Content-Length', -1)) 191 conn.close() 192 return content_length 193 194 195def download_file(download: Tuple[str, str, str]) -> str: 196 label, url, file_path = download 197 parsed_url = urllib.parse.urlparse(url) 198 conn = connect(parsed_url) 199 conn.request('GET', parsed_url.path) 200 r = conn.getresponse() 201 if r.status == 200: 202 with open(file_path, 'wb') as f: 203 shutil.copyfileobj(r, f) 204 conn.close() 205 return label 206 207 208if __name__ == '__main__': 209 main()