TonieToolbox 0.5.0a1__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.
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/python3
2
+ # TODO: Add integration_ubuntu.py