TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- TonieToolbox/__init__.py +2 -1
- TonieToolbox/__main__.py +240 -98
- TonieToolbox/artwork.py +59 -10
- TonieToolbox/audio_conversion.py +33 -29
- TonieToolbox/constants.py +133 -10
- TonieToolbox/dependency_manager.py +679 -184
- TonieToolbox/filename_generator.py +57 -10
- TonieToolbox/integration.py +73 -0
- TonieToolbox/integration_macos.py +613 -0
- TonieToolbox/integration_ubuntu.py +2 -0
- TonieToolbox/integration_windows.py +445 -0
- TonieToolbox/logger.py +9 -10
- TonieToolbox/media_tags.py +19 -100
- TonieToolbox/ogg_page.py +41 -41
- TonieToolbox/opus_packet.py +15 -15
- TonieToolbox/recursive_processor.py +24 -23
- TonieToolbox/tags.py +4 -5
- TonieToolbox/teddycloud.py +164 -51
- TonieToolbox/tonie_analysis.py +26 -24
- TonieToolbox/tonie_file.py +73 -45
- TonieToolbox/tonies_json.py +71 -67
- TonieToolbox/version_handler.py +14 -20
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/METADATA +129 -92
- tonietoolbox-0.6.0.dist-info/RECORD +30 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.5.1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,613 @@
|
|
1
|
+
#!/usr/bin/python3
|
2
|
+
"""
|
3
|
+
Integration for MacOS Quick Actions (Services) for TonieToolbox.
|
4
|
+
This module provides functionality to create and manage Quick Actions.
|
5
|
+
"""
|
6
|
+
import os
|
7
|
+
import sys
|
8
|
+
import json
|
9
|
+
import plistlib
|
10
|
+
import subprocess
|
11
|
+
from pathlib import Path
|
12
|
+
from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE,UTI_MAPPINGS,ICON_BASE64
|
13
|
+
from .artwork import base64_to_ico
|
14
|
+
from .logger import get_logger
|
15
|
+
|
16
|
+
logger = get_logger(__name__)
|
17
|
+
|
18
|
+
class MacOSContextMenuIntegration:
|
19
|
+
"""
|
20
|
+
Class to generate macOS Quick Actions for TonieToolbox integration.
|
21
|
+
Creates Quick Actions (Services) for supported audio files, .taf files, and folders.
|
22
|
+
"""
|
23
|
+
def __init__(self):
|
24
|
+
# Find the installed command-line tool path
|
25
|
+
self.exe_path = os.path.join(sys.prefix, 'bin', 'tonietoolbox')
|
26
|
+
self.output_dir = os.path.join(os.path.expanduser('~'), '.tonietoolbox')
|
27
|
+
self.services_dir = os.path.join(os.path.expanduser('~'), 'Library', 'Services')
|
28
|
+
self.icon_path = os.path.join(self.output_dir, 'icon.png')
|
29
|
+
os.makedirs(self.output_dir, exist_ok=True)
|
30
|
+
self.error_handling = 'if [ $? -ne 0 ]; then\n echo "Error: Command failed with error code $?"\n read -p "Press any key to close this window..." key\n exit 1\nfi'
|
31
|
+
self.success_handling = 'echo "Command completed successfully"\nsleep 2'
|
32
|
+
self.config = self._apply_config_template()
|
33
|
+
self.upload_url = ''
|
34
|
+
self.log_level = self.config.get('log_level', 'SILENT')
|
35
|
+
self.log_to_file = self.config.get('log_to_file', False)
|
36
|
+
self.basic_authentication_cmd = ''
|
37
|
+
self.client_cert_cmd = ''
|
38
|
+
self.upload_enabled = self._setup_upload()
|
39
|
+
|
40
|
+
logger.debug(f"Upload enabled: {self.upload_enabled}")
|
41
|
+
logger.debug(f"Upload URL: {self.upload_url}")
|
42
|
+
logger.debug(f"Authentication: {'Basic Authentication' if self.basic_authentication else ('None' if self.none_authentication else ('Client Cert' if self.client_cert_authentication else 'Unknown'))}")
|
43
|
+
self._setup_commands()
|
44
|
+
|
45
|
+
def _build_cmd(self, base_args, file_placeholder='$1', output_to_source=True, use_upload=False, use_artwork=False, use_json=False, use_compare=False, use_info=False, is_recursive=False, is_split=False, is_folder=False, keep_open=False, log_to_file=False):
|
46
|
+
"""Dynamically build command strings for quick actions."""
|
47
|
+
exe = self.exe_path
|
48
|
+
cmd = '#!/bin/bash\n\n'
|
49
|
+
# Debug output to see what's being passed to the script
|
50
|
+
cmd += 'echo "Arguments received: $@"\n'
|
51
|
+
cmd += 'echo "Number of arguments: $#"\n'
|
52
|
+
cmd += 'if [ $# -gt 0 ]; then\n'
|
53
|
+
cmd += ' echo "First argument: $1"\n'
|
54
|
+
cmd += 'fi\n\n'
|
55
|
+
|
56
|
+
# Add a description of what's being executed
|
57
|
+
cmd += 'echo "Running TonieToolbox'
|
58
|
+
if use_info:
|
59
|
+
cmd += ' info'
|
60
|
+
elif is_split:
|
61
|
+
cmd += ' split'
|
62
|
+
elif use_compare:
|
63
|
+
cmd += ' compare'
|
64
|
+
elif is_recursive:
|
65
|
+
cmd += ' recursive folder convert'
|
66
|
+
elif is_folder:
|
67
|
+
cmd += ' folder convert'
|
68
|
+
elif use_upload and use_artwork and use_json:
|
69
|
+
cmd += ' convert, upload, artwork and JSON'
|
70
|
+
elif use_upload and use_artwork:
|
71
|
+
cmd += ' convert, upload and artwork'
|
72
|
+
elif use_upload:
|
73
|
+
cmd += ' convert and upload'
|
74
|
+
else:
|
75
|
+
cmd += ' convert'
|
76
|
+
cmd += ' command..."\n\n'
|
77
|
+
# Properly handle paths from macOS Services
|
78
|
+
if is_folder or is_recursive:
|
79
|
+
# Handle multiple arguments and ensure we get a valid folder
|
80
|
+
cmd += '# Handle paths from macOS Services\n'
|
81
|
+
cmd += '# First, try to get paths from stdin (macOS passes paths this way)\n'
|
82
|
+
cmd += 'if [ -p /dev/stdin ]; then\n'
|
83
|
+
cmd += ' PATHS=$(cat /dev/stdin)\n'
|
84
|
+
cmd += ' echo "Found paths from stdin: $PATHS"\n'
|
85
|
+
cmd += 'fi\n\n'
|
86
|
+
cmd += '# If no paths from stdin, check command line arguments\n'
|
87
|
+
cmd += 'FOLDER_PATH=""\n'
|
88
|
+
cmd += 'if [ -z "$PATHS" ]; then\n'
|
89
|
+
cmd += ' for arg in "$@"; do\n'
|
90
|
+
cmd += ' if [ -d "$arg" ]; then\n'
|
91
|
+
cmd += ' FOLDER_PATH="$arg"\n'
|
92
|
+
cmd += ' echo "Processing folder from args: $FOLDER_PATH"\n'
|
93
|
+
cmd += ' break\n'
|
94
|
+
cmd += ' fi\n'
|
95
|
+
cmd += ' done\n'
|
96
|
+
cmd += 'else\n'
|
97
|
+
cmd += ' for path in $PATHS; do\n'
|
98
|
+
cmd += ' if [ -d "$path" ]; then\n'
|
99
|
+
cmd += ' FOLDER_PATH="$path"\n'
|
100
|
+
cmd += ' echo "Processing folder from stdin: $FOLDER_PATH"\n'
|
101
|
+
cmd += ' break\n'
|
102
|
+
cmd += ' fi\n'
|
103
|
+
cmd += ' done\n'
|
104
|
+
cmd += 'fi\n\n'
|
105
|
+
cmd += 'if [ -z "$FOLDER_PATH" ]; then\n'
|
106
|
+
cmd += ' echo "Error: No valid folder path found in arguments or stdin"\n'
|
107
|
+
cmd += ' read -p "Press any key to close this window..." key\n'
|
108
|
+
cmd += ' exit 1\n'
|
109
|
+
cmd += 'fi\n\n'
|
110
|
+
|
111
|
+
# Use the variable for the command
|
112
|
+
file_placeholder='$FOLDER_PATH'
|
113
|
+
elif use_compare:
|
114
|
+
# For compare operation, we need two file paths
|
115
|
+
cmd += '# Compare requires two files\n'
|
116
|
+
cmd += 'if [ $# -lt 2 ]; then\n'
|
117
|
+
cmd += ' echo "Error: Compare operation requires two files."\n'
|
118
|
+
cmd += ' read -p "Press any key to close this window..." key\n'
|
119
|
+
cmd += ' exit 1\n'
|
120
|
+
cmd += 'fi\n\n'
|
121
|
+
else:
|
122
|
+
# For regular file operations, handle paths correctly
|
123
|
+
cmd += '# Handle file paths correctly - try multiple methods for macOS\n'
|
124
|
+
cmd += 'FILE_PATH=""\n'
|
125
|
+
|
126
|
+
# First, try to get paths from stdin (macOS passes paths this way sometimes)
|
127
|
+
cmd += '# Method 1: Try to read from stdin if available\n'
|
128
|
+
cmd += 'if [ -p /dev/stdin ]; then\n'
|
129
|
+
cmd += ' STDIN_PATHS=$(cat)\n'
|
130
|
+
cmd += ' if [ -n "$STDIN_PATHS" ]; then\n'
|
131
|
+
cmd += ' for path in $STDIN_PATHS; do\n'
|
132
|
+
cmd += ' if [ -f "$path" ]; then\n'
|
133
|
+
cmd += ' FILE_PATH="$path"\n'
|
134
|
+
cmd += ' echo "Found file path from stdin: $FILE_PATH"\n'
|
135
|
+
cmd += ' break\n'
|
136
|
+
cmd += ' fi\n'
|
137
|
+
cmd += ' done\n'
|
138
|
+
cmd += ' fi\n'
|
139
|
+
cmd += 'fi\n\n'
|
140
|
+
|
141
|
+
# Method 2: Try command line arguments
|
142
|
+
cmd += '# Method 2: Check command line arguments\n'
|
143
|
+
cmd += 'if [ -z "$FILE_PATH" ]; then\n'
|
144
|
+
cmd += ' for arg in "$@"; do\n'
|
145
|
+
cmd += ' if [ -f "$arg" ]; then\n'
|
146
|
+
cmd += ' FILE_PATH="$arg"\n'
|
147
|
+
cmd += ' echo "Found file path from arguments: $FILE_PATH"\n'
|
148
|
+
cmd += ' break\n'
|
149
|
+
cmd += ' fi\n'
|
150
|
+
cmd += ' done\n'
|
151
|
+
cmd += 'fi\n\n'
|
152
|
+
|
153
|
+
# Method 3: Try to handle case where path might be in $1
|
154
|
+
cmd += '# Method 3: Try first argument directly\n'
|
155
|
+
cmd += 'if [ -z "$FILE_PATH" ] && [ -n "$1" ] && [ -f "$1" ]; then\n'
|
156
|
+
cmd += ' FILE_PATH="$1"\n'
|
157
|
+
cmd += ' echo "Using first argument directly as file path: $FILE_PATH"\n'
|
158
|
+
cmd += 'fi\n\n'
|
159
|
+
|
160
|
+
# Method 4: Parse automator's encoded path format
|
161
|
+
cmd += '# Method 4: Try to decode special format macOS might use\n'
|
162
|
+
cmd += 'if [ -z "$FILE_PATH" ] && [ -n "$1" ]; then\n'
|
163
|
+
cmd += ' # Sometimes macOS passes paths with "file://" prefix\n'
|
164
|
+
cmd += ' DECODED_PATH=$(echo "$1" | sed -e "s|^file://||" -e "s|%20| |g")\n'
|
165
|
+
cmd += ' if [ -f "$DECODED_PATH" ]; then\n'
|
166
|
+
cmd += ' FILE_PATH="$DECODED_PATH"\n'
|
167
|
+
cmd += ' echo "Using decoded path: $FILE_PATH"\n'
|
168
|
+
cmd += ' fi\n'
|
169
|
+
cmd += 'fi\n\n'
|
170
|
+
|
171
|
+
# Final check
|
172
|
+
cmd += 'if [ -z "$FILE_PATH" ]; then\n'
|
173
|
+
cmd += ' echo "Error: Could not find a valid file path. Tried:"\n'
|
174
|
+
cmd += ' echo "- Reading from stdin"\n'
|
175
|
+
cmd += ' echo "- Command arguments: $@"\n'
|
176
|
+
cmd += ' echo "- Decoding URL format"\n'
|
177
|
+
cmd += ' read -p "Press any key to close this window..." key\n'
|
178
|
+
cmd += ' exit 1\n'
|
179
|
+
cmd += 'fi\n\n'
|
180
|
+
|
181
|
+
# Use the variable for the command
|
182
|
+
file_placeholder='$FILE_PATH'
|
183
|
+
|
184
|
+
# Build the actual command
|
185
|
+
cmd_line = f'"{exe}" {base_args}'
|
186
|
+
if log_to_file:
|
187
|
+
cmd_line += ' --log-file'
|
188
|
+
if is_recursive:
|
189
|
+
cmd_line += ' --recursive'
|
190
|
+
if output_to_source:
|
191
|
+
cmd_line += ' --output-to-source'
|
192
|
+
if use_info:
|
193
|
+
cmd_line += ' --info'
|
194
|
+
if is_split:
|
195
|
+
cmd_line += ' --split'
|
196
|
+
if use_compare:
|
197
|
+
# For compare, we need to handle two files
|
198
|
+
cmd += '# Find two TAF files for comparison\n'
|
199
|
+
cmd += 'FILE1=""\n'
|
200
|
+
cmd += 'FILE2=""\n'
|
201
|
+
cmd += 'for arg in "$@"; do\n'
|
202
|
+
cmd += ' if [ -f "$arg" ]; then\n'
|
203
|
+
cmd += ' if [ -z "$FILE1" ]; then\n'
|
204
|
+
cmd += ' FILE1="$arg"\n'
|
205
|
+
cmd += ' echo "First TAF file: $FILE1"\n'
|
206
|
+
cmd += ' elif [ -z "$FILE2" ]; then\n'
|
207
|
+
cmd += ' FILE2="$arg"\n'
|
208
|
+
cmd += ' echo "Second TAF file: $FILE2"\n'
|
209
|
+
cmd += ' break\n'
|
210
|
+
cmd += ' fi\n'
|
211
|
+
cmd += ' fi\n'
|
212
|
+
cmd += 'done\n\n'
|
213
|
+
cmd += 'if [ -z "$FILE1" ] || [ -z "$FILE2" ]; then\n'
|
214
|
+
cmd += ' echo "Error: Need two TAF files for comparison."\n'
|
215
|
+
cmd += ' read -p "Press any key to close this window..." key\n'
|
216
|
+
cmd += ' exit 1\n'
|
217
|
+
cmd += 'fi\n\n'
|
218
|
+
cmd_line += ' --compare "$FILE1" "$FILE2"'
|
219
|
+
else:
|
220
|
+
cmd_line += f' "{file_placeholder}"'
|
221
|
+
if use_upload:
|
222
|
+
cmd_line += f' --upload "{self.upload_url}"'
|
223
|
+
if self.basic_authentication_cmd:
|
224
|
+
cmd_line += f' {self.basic_authentication_cmd}'
|
225
|
+
elif self.client_cert_cmd:
|
226
|
+
cmd_line += f' {self.client_cert_cmd}'
|
227
|
+
if getattr(self, "ignore_ssl_verify", False):
|
228
|
+
cmd_line += ' --ignore-ssl-verify'
|
229
|
+
if use_artwork:
|
230
|
+
cmd_line += ' --include-artwork'
|
231
|
+
if use_json:
|
232
|
+
cmd_line += ' --create-custom-json'
|
233
|
+
|
234
|
+
# Add the command to the script
|
235
|
+
cmd += f'echo "Executing: {cmd_line}"\n'
|
236
|
+
cmd += f'{cmd_line}\n\n'
|
237
|
+
|
238
|
+
# Add error and success handling
|
239
|
+
cmd += f'{self.error_handling}\n\n'
|
240
|
+
if use_info or use_compare or keep_open:
|
241
|
+
cmd += 'echo ""\nread -p "Press any key to close this window..." key\n'
|
242
|
+
else:
|
243
|
+
cmd += f'{self.success_handling}\n'
|
244
|
+
|
245
|
+
return cmd
|
246
|
+
|
247
|
+
def _get_log_level_arg(self):
|
248
|
+
"""Return the correct log level argument for TonieToolbox CLI based on self.log_level."""
|
249
|
+
level = str(self.log_level).strip().upper()
|
250
|
+
if level == 'DEBUG':
|
251
|
+
return '--debug'
|
252
|
+
elif level == 'INFO':
|
253
|
+
return '--info'
|
254
|
+
return '--silent'
|
255
|
+
|
256
|
+
def _setup_commands(self):
|
257
|
+
"""Set up all command strings for quick actions dynamically."""
|
258
|
+
log_level_arg = self._get_log_level_arg()
|
259
|
+
|
260
|
+
# Audio file commands
|
261
|
+
self.convert_cmd = self._build_cmd(f'{log_level_arg}', log_to_file=self.log_to_file)
|
262
|
+
self.upload_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, log_to_file=self.log_to_file)
|
263
|
+
self.upload_artwork_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
|
264
|
+
self.upload_artwork_json_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
|
265
|
+
|
266
|
+
# .taf file commands
|
267
|
+
self.show_info_cmd = self._build_cmd(log_level_arg, use_info=True, keep_open=True, log_to_file=self.log_to_file)
|
268
|
+
self.extract_opus_cmd = self._build_cmd(log_level_arg, is_split=True, log_to_file=self.log_to_file)
|
269
|
+
self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
|
270
|
+
self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
|
271
|
+
self.upload_taf_artwork_json_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
|
272
|
+
self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
|
273
|
+
|
274
|
+
# Folder commands
|
275
|
+
self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
|
276
|
+
self.upload_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, log_to_file=self.log_to_file)
|
277
|
+
self.upload_folder_artwork_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
|
278
|
+
self.upload_folder_artwork_json_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, use_upload=True, use_artwork=True, use_json=True, log_to_file=self.log_to_file)
|
279
|
+
|
280
|
+
def _apply_config_template(self):
|
281
|
+
"""Apply the default configuration template if config.json is missing or invalid. Extracts the icon from base64 if not present."""
|
282
|
+
config_path = os.path.join(self.output_dir, 'config.json')
|
283
|
+
icon_path = os.path.join(self.output_dir, 'icon.ico')
|
284
|
+
if not os.path.exists(icon_path):
|
285
|
+
base64_to_ico(ICON_BASE64, icon_path)
|
286
|
+
if not os.path.exists(config_path):
|
287
|
+
with open(config_path, 'w') as f:
|
288
|
+
json.dump(CONFIG_TEMPLATE, f, indent=4)
|
289
|
+
logger.debug(f"Default configuration created at {config_path}")
|
290
|
+
return CONFIG_TEMPLATE
|
291
|
+
else:
|
292
|
+
logger.debug(f"Configuration file found at {config_path}")
|
293
|
+
return self._load_config()
|
294
|
+
|
295
|
+
def _load_config(self):
|
296
|
+
"""Load configuration settings from config.json"""
|
297
|
+
config_path = os.path.join(self.output_dir, 'config.json')
|
298
|
+
if not os.path.exists(config_path):
|
299
|
+
logger.debug(f"Configuration file not found: {config_path}")
|
300
|
+
return {}
|
301
|
+
|
302
|
+
try:
|
303
|
+
with open(config_path, 'r') as f:
|
304
|
+
config = json.loads(f.read())
|
305
|
+
return config
|
306
|
+
except (json.JSONDecodeError, IOError) as e:
|
307
|
+
logger.debug(f"Error loading config: {e}")
|
308
|
+
return {}
|
309
|
+
|
310
|
+
def _setup_upload(self):
|
311
|
+
"""Set up upload functionality based on config.json settings"""
|
312
|
+
self.basic_authentication = False
|
313
|
+
self.client_cert_authentication = False
|
314
|
+
self.none_authentication = False
|
315
|
+
|
316
|
+
config = self.config
|
317
|
+
try:
|
318
|
+
upload_config = config.get('upload', {})
|
319
|
+
self.upload_urls = upload_config.get('url', [])
|
320
|
+
self.ignore_ssl_verify = upload_config.get('ignore_ssl_verify', False)
|
321
|
+
self.username = upload_config.get('username', '')
|
322
|
+
self.password = upload_config.get('password', '')
|
323
|
+
self.basic_authentication_cmd = ''
|
324
|
+
self.client_cert_cmd = ''
|
325
|
+
|
326
|
+
if self.username and self.password:
|
327
|
+
self.basic_authentication_cmd = f'--username {self.username} --password {self.password}'
|
328
|
+
self.basic_authentication = True
|
329
|
+
|
330
|
+
self.client_cert_path = upload_config.get('client_cert_path', '')
|
331
|
+
self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
|
332
|
+
if self.client_cert_path and self.client_cert_key_path:
|
333
|
+
self.client_cert_cmd = f'--client-cert {self.client_cert_path} --client-cert-key {self.client_cert_key_path}'
|
334
|
+
self.client_cert_authentication = True
|
335
|
+
|
336
|
+
if self.client_cert_authentication and self.basic_authentication:
|
337
|
+
logger.warning("Both client certificate and basic authentication are set. Only one can be used.")
|
338
|
+
return False
|
339
|
+
|
340
|
+
self.upload_url = self.upload_urls[0] if self.upload_urls else ''
|
341
|
+
if not self.client_cert_authentication and not self.basic_authentication and self.upload_url:
|
342
|
+
self.none_authentication = True
|
343
|
+
|
344
|
+
return bool(self.upload_url)
|
345
|
+
except Exception as e:
|
346
|
+
logger.debug(f"Unexpected error while loading configuration: {e}")
|
347
|
+
return False
|
348
|
+
def _create_quick_action(self, name, command, file_types=None, directory_based=False):
|
349
|
+
"""Create a macOS Quick Action (Service) with the given name and command."""
|
350
|
+
action_dir = os.path.join(self.services_dir, f"{name}.workflow")
|
351
|
+
os.makedirs(action_dir, exist_ok=True)
|
352
|
+
contents_dir = os.path.join(action_dir, "Contents")
|
353
|
+
os.makedirs(contents_dir, exist_ok=True)
|
354
|
+
document_path = os.path.join(contents_dir, "document.wflow")
|
355
|
+
|
356
|
+
# Set up the plist to ensure the service appears in context menus
|
357
|
+
info_plist = {
|
358
|
+
"NSServices": [
|
359
|
+
{
|
360
|
+
"NSMenuItem": {
|
361
|
+
"default": name
|
362
|
+
},
|
363
|
+
"NSMessage": "runWorkflowAsService",
|
364
|
+
"NSRequiredContext": {
|
365
|
+
"NSApplicationIdentifier": "com.apple.finder"
|
366
|
+
},
|
367
|
+
"NSSendFileTypes": file_types if file_types else [],
|
368
|
+
"NSSendTypes": ["NSFilenamesPboardType"], # Always include this to ensure paths are passed correctly
|
369
|
+
"NSUserData": name,
|
370
|
+
"NSExecutable": "script", # Ensure macOS knows which script to run
|
371
|
+
"NSReturnTypes": []
|
372
|
+
}
|
373
|
+
]
|
374
|
+
}
|
375
|
+
|
376
|
+
info_path = os.path.join(contents_dir, "Info.plist")
|
377
|
+
with open(info_path, "wb") as f:
|
378
|
+
plistlib.dump(info_plist, f)
|
379
|
+
script_dir = os.path.join(contents_dir, "MacOS")
|
380
|
+
os.makedirs(script_dir, exist_ok=True)
|
381
|
+
script_path = os.path.join(script_dir, "script")
|
382
|
+
|
383
|
+
with open(script_path, "w") as f:
|
384
|
+
f.write(command)
|
385
|
+
os.chmod(script_path, 0o755)
|
386
|
+
workflow = {
|
387
|
+
"AMApplication": "Automator",
|
388
|
+
"AMCanShowSelectedItemsWhenRun": True,
|
389
|
+
"AMCanShowWhenRun": True,
|
390
|
+
"AMDockBadgeLabel": "",
|
391
|
+
"AMDockBadgeStyle": "badge",
|
392
|
+
"AMName": name,
|
393
|
+
"AMRootElement": {
|
394
|
+
"actions": [
|
395
|
+
{
|
396
|
+
"action": "run-shell-script",
|
397
|
+
"parameters": {
|
398
|
+
"shell": "/bin/bash",
|
399
|
+
"script": command,
|
400
|
+
"input": "as arguments",
|
401
|
+
"showStdout": True,
|
402
|
+
"showStderr": True,
|
403
|
+
"showOutput": True,
|
404
|
+
"runAsAdmin": False
|
405
|
+
}
|
406
|
+
}
|
407
|
+
],
|
408
|
+
"class": "workflow",
|
409
|
+
"connections": {},
|
410
|
+
"id": "workflow-element",
|
411
|
+
"title": name
|
412
|
+
},
|
413
|
+
"AMWorkflowSchemeVersion": 2.0,
|
414
|
+
}
|
415
|
+
with open(document_path, "wb") as f:
|
416
|
+
plistlib.dump(workflow, f)
|
417
|
+
|
418
|
+
return action_dir
|
419
|
+
|
420
|
+
def _extension_to_uti(self, extension):
|
421
|
+
"""Convert a file extension to macOS UTI (Uniform Type Identifier)."""
|
422
|
+
uti_map = UTI_MAPPINGS
|
423
|
+
ext = extension.lower().lstrip('.')
|
424
|
+
return uti_map.get(ext, f'public.{ext}')
|
425
|
+
|
426
|
+
def _generate_audio_extension_actions(self):
|
427
|
+
"""Generate Quick Actions for supported audio file extensions."""
|
428
|
+
extensions = [ext.lower().lstrip('.') for ext in SUPPORTED_EXTENSIONS]
|
429
|
+
# Convert extensions to UTIs (Uniform Type Identifiers)
|
430
|
+
utis = [self._extension_to_uti(ext) for ext in extensions]
|
431
|
+
self._create_quick_action(
|
432
|
+
"TonieToolbox - Convert to TAF",
|
433
|
+
self.convert_cmd,
|
434
|
+
file_types=utis
|
435
|
+
)
|
436
|
+
|
437
|
+
if self.upload_enabled:
|
438
|
+
self._create_quick_action(
|
439
|
+
"TonieToolbox - Convert and Upload",
|
440
|
+
self.upload_cmd,
|
441
|
+
file_types=utis
|
442
|
+
)
|
443
|
+
|
444
|
+
self._create_quick_action(
|
445
|
+
"TonieToolbox - Convert, Upload with Artwork",
|
446
|
+
self.upload_artwork_cmd,
|
447
|
+
file_types=utis
|
448
|
+
)
|
449
|
+
|
450
|
+
self._create_quick_action(
|
451
|
+
"TonieToolbox - Convert, Upload with Artwork and JSON",
|
452
|
+
self.upload_artwork_json_cmd,
|
453
|
+
file_types=utis
|
454
|
+
)
|
455
|
+
|
456
|
+
def _generate_taf_file_actions(self):
|
457
|
+
"""Generate Quick Actions for .taf files."""
|
458
|
+
taf_uti = self._extension_to_uti("taf") # Use UTI for TAF files
|
459
|
+
|
460
|
+
self._create_quick_action(
|
461
|
+
"TonieToolbox - Show Info",
|
462
|
+
self.show_info_cmd,
|
463
|
+
file_types=[taf_uti]
|
464
|
+
)
|
465
|
+
|
466
|
+
self._create_quick_action(
|
467
|
+
"TonieToolbox - Extract Opus Tracks",
|
468
|
+
self.extract_opus_cmd,
|
469
|
+
file_types=[taf_uti]
|
470
|
+
)
|
471
|
+
|
472
|
+
if self.upload_enabled:
|
473
|
+
self._create_quick_action(
|
474
|
+
"TonieToolbox - Upload",
|
475
|
+
self.upload_taf_cmd,
|
476
|
+
file_types=[taf_uti]
|
477
|
+
)
|
478
|
+
self._create_quick_action(
|
479
|
+
"TonieToolbox - Upload with Artwork",
|
480
|
+
self.upload_taf_artwork_cmd,
|
481
|
+
file_types=[taf_uti]
|
482
|
+
)
|
483
|
+
|
484
|
+
self._create_quick_action(
|
485
|
+
"TonieToolbox - Upload with Artwork and JSON",
|
486
|
+
self.upload_taf_artwork_json_cmd,
|
487
|
+
file_types=[taf_uti]
|
488
|
+
)
|
489
|
+
|
490
|
+
self._create_quick_action(
|
491
|
+
"TonieToolbox - Compare with another TAF file",
|
492
|
+
self.compare_taf_cmd,
|
493
|
+
file_types=[taf_uti]
|
494
|
+
)
|
495
|
+
|
496
|
+
def _generate_folder_actions(self):
|
497
|
+
"""Generate Quick Actions for folders."""
|
498
|
+
self._create_quick_action(
|
499
|
+
"TonieToolbox - 1. Convert Folder to TAF (recursive)",
|
500
|
+
self.convert_folder_cmd,
|
501
|
+
directory_based=True
|
502
|
+
)
|
503
|
+
|
504
|
+
if self.upload_enabled:
|
505
|
+
self._create_quick_action(
|
506
|
+
"TonieToolbox - 2. Convert Folder and Upload (recursive)",
|
507
|
+
self.upload_folder_cmd,
|
508
|
+
directory_based=True
|
509
|
+
)
|
510
|
+
|
511
|
+
self._create_quick_action(
|
512
|
+
"TonieToolbox - 3. Convert Folder, Upload with Artwork (recursive)",
|
513
|
+
self.upload_folder_artwork_cmd,
|
514
|
+
directory_based=True
|
515
|
+
)
|
516
|
+
|
517
|
+
self._create_quick_action(
|
518
|
+
"TonieToolbox - 4. Convert Folder, Upload with Artwork and JSON (recursive)",
|
519
|
+
self.upload_folder_artwork_json_cmd,
|
520
|
+
directory_based=True
|
521
|
+
)
|
522
|
+
|
523
|
+
def install_quick_actions(self):
|
524
|
+
"""
|
525
|
+
Install all Quick Actions.
|
526
|
+
|
527
|
+
Returns:
|
528
|
+
bool: True if all actions were installed successfully, False otherwise.
|
529
|
+
"""
|
530
|
+
try:
|
531
|
+
# Ensure Services directory exists
|
532
|
+
os.makedirs(self.services_dir, exist_ok=True)
|
533
|
+
|
534
|
+
# Check if the icon exists, copy default if needed
|
535
|
+
if not os.path.exists(self.icon_path):
|
536
|
+
# Include code to extract icon from resources
|
537
|
+
logger.debug(f"Icon not found at {self.icon_path}, using default")
|
538
|
+
|
539
|
+
# Generate Quick Actions for different file types
|
540
|
+
self._generate_audio_extension_actions()
|
541
|
+
self._generate_taf_file_actions()
|
542
|
+
self._generate_folder_actions()
|
543
|
+
|
544
|
+
# Refresh the Services menu by restarting the Finder
|
545
|
+
result = subprocess.run(["killall", "-HUP", "Finder"], check=False,
|
546
|
+
capture_output=True, text=True)
|
547
|
+
logger.info("TonieToolbox Quick Actions installed successfully.")
|
548
|
+
logger.info("You'll find them in the Services menu when right-clicking on audio files, TAF files, or folders.")
|
549
|
+
|
550
|
+
return True
|
551
|
+
except Exception as e:
|
552
|
+
logger.error(f"Failed to install Quick Actions: {e}")
|
553
|
+
return False
|
554
|
+
|
555
|
+
def uninstall_quick_actions(self):
|
556
|
+
"""
|
557
|
+
Uninstall all TonieToolbox Quick Actions.
|
558
|
+
|
559
|
+
Returns:
|
560
|
+
bool: True if all actions were uninstalled successfully, False otherwise.
|
561
|
+
"""
|
562
|
+
try:
|
563
|
+
any_failures = False
|
564
|
+
for item in os.listdir(self.services_dir):
|
565
|
+
if item.startswith("TonieToolbox - ") and item.endswith(".workflow"):
|
566
|
+
action_path = os.path.join(self.services_dir, item)
|
567
|
+
try:
|
568
|
+
subprocess.run(["rm", "-rf", action_path], check=True)
|
569
|
+
print(f"Removed: {item}")
|
570
|
+
except subprocess.CalledProcessError as e:
|
571
|
+
print(f"Failed to remove: {item}")
|
572
|
+
logger.error(f"Error removing {item}: {e}")
|
573
|
+
any_failures = True
|
574
|
+
subprocess.run(["killall", "-HUP", "Finder"], check=False)
|
575
|
+
|
576
|
+
print("TonieToolbox Quick Actions uninstalled successfully.")
|
577
|
+
|
578
|
+
return not any_failures
|
579
|
+
except Exception as e:
|
580
|
+
logger.error(f"Failed to uninstall Quick Actions: {e}")
|
581
|
+
return False
|
582
|
+
|
583
|
+
@classmethod
|
584
|
+
def install(cls):
|
585
|
+
"""
|
586
|
+
Generate Quick Actions and install them.
|
587
|
+
|
588
|
+
Returns:
|
589
|
+
bool: True if installation was successful, False otherwise.
|
590
|
+
"""
|
591
|
+
instance = cls()
|
592
|
+
if instance.install_quick_actions():
|
593
|
+
logger.info("macOS integration installed successfully.")
|
594
|
+
return True
|
595
|
+
else:
|
596
|
+
logger.error("macOS integration installation failed.")
|
597
|
+
return False
|
598
|
+
|
599
|
+
@classmethod
|
600
|
+
def uninstall(cls):
|
601
|
+
"""
|
602
|
+
Uninstall all TonieToolbox Quick Actions.
|
603
|
+
|
604
|
+
Returns:
|
605
|
+
bool: True if uninstallation was successful, False otherwise.
|
606
|
+
"""
|
607
|
+
instance = cls()
|
608
|
+
if instance.uninstall_quick_actions():
|
609
|
+
logger.info("macOS integration uninstalled successfully.")
|
610
|
+
return True
|
611
|
+
else:
|
612
|
+
logger.error("macOS integration uninstallation failed.")
|
613
|
+
return False
|