[READ-ONLY] Mirror of https://github.com/mrgnw/scripts.
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()