TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0a2__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,477 @@
1
+ # filepath: d:\Repository\TonieToolbox\TonieToolbox\integration_macos.py
2
+ import os
3
+ import sys
4
+ import json
5
+ import plistlib
6
+ import subprocess
7
+ from pathlib import Path
8
+ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE
9
+ from .logger import get_logger
10
+
11
+ logger = get_logger('integration_macos')
12
+
13
+ class MacOSContextMenuIntegration:
14
+ """
15
+ Class to generate macOS Quick Actions for TonieToolbox integration.
16
+ Creates Quick Actions (Services) for supported audio files, .taf files, and folders.
17
+ """
18
+ def __init__(self):
19
+ # Find the installed command-line tool path
20
+ self.exe_path = os.path.join(sys.prefix, 'bin', 'tonietoolbox')
21
+ self.output_dir = os.path.join(os.path.expanduser('~'), '.tonietoolbox')
22
+ self.services_dir = os.path.join(os.path.expanduser('~'), 'Library', 'Services')
23
+ self.icon_path = os.path.join(self.output_dir, 'icon.png')
24
+ os.makedirs(self.output_dir, exist_ok=True)
25
+
26
+ # Error handling and success messages for shell scripts
27
+ 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'
28
+ self.success_handling = 'echo "Command completed successfully"\nsleep 2'
29
+
30
+ # Load configuration
31
+ self.config = self._apply_config_template()
32
+
33
+ # Ensure these attributes always exist
34
+ self.upload_url = ''
35
+ self.log_level = self.config.get('log_level', 'SILENT')
36
+ self.log_to_file = self.config.get('log_to_file', False)
37
+ self.basic_authentication_cmd = ''
38
+ self.client_cert_cmd = ''
39
+ self.upload_enabled = self._setup_upload()
40
+
41
+ logger.debug(f"Upload enabled: {self.upload_enabled}")
42
+ logger.debug(f"Upload URL: {self.upload_url}")
43
+ 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'))}")
44
+
45
+ self._setup_commands()
46
+
47
+ 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):
48
+ """Dynamically build command strings for quick actions."""
49
+ exe = self.exe_path
50
+ cmd = '#!/bin/bash\n\n'
51
+
52
+ # Add a description of what's being executed
53
+ cmd += 'echo "Running TonieToolbox'
54
+ if use_info:
55
+ cmd += ' info'
56
+ elif is_split:
57
+ cmd += ' split'
58
+ elif use_compare:
59
+ cmd += ' compare'
60
+ elif is_recursive:
61
+ cmd += ' recursive folder convert'
62
+ elif is_folder:
63
+ cmd += ' folder convert'
64
+ elif use_upload and use_artwork and use_json:
65
+ cmd += ' convert, upload, artwork and JSON'
66
+ elif use_upload and use_artwork:
67
+ cmd += ' convert, upload and artwork'
68
+ elif use_upload:
69
+ cmd += ' convert and upload'
70
+ else:
71
+ cmd += ' convert'
72
+ cmd += ' command..."\n\n'
73
+
74
+ # Build the actual command
75
+ cmd_line = f'"{exe}" {base_args}'
76
+ if log_to_file:
77
+ cmd_line += ' --log-file'
78
+ if is_recursive:
79
+ cmd_line += ' --recursive'
80
+ if output_to_source:
81
+ cmd_line += ' --output-to-source'
82
+ if use_info:
83
+ cmd_line += ' --info'
84
+ if is_split:
85
+ cmd_line += ' --split'
86
+ if use_compare:
87
+ cmd_line += ' --compare "$1" "$2"'
88
+ else:
89
+ cmd_line += f' "{file_placeholder}"'
90
+ if use_upload:
91
+ cmd_line += f' --upload "{self.upload_url}"'
92
+ if self.basic_authentication_cmd:
93
+ cmd_line += f' {self.basic_authentication_cmd}'
94
+ elif self.client_cert_cmd:
95
+ cmd_line += f' {self.client_cert_cmd}'
96
+ if getattr(self, "ignore_ssl_verify", False):
97
+ cmd_line += ' --ignore-ssl-verify'
98
+ if use_artwork:
99
+ cmd_line += ' --include-artwork'
100
+ if use_json:
101
+ cmd_line += ' --create-custom-json'
102
+
103
+ # Add the command to the script
104
+ cmd += f'{cmd_line}\n\n'
105
+
106
+ # Add error and success handling
107
+ cmd += f'{self.error_handling}\n\n'
108
+ if use_info or use_compare or keep_open:
109
+ cmd += 'echo ""\nread -p "Press any key to close this window..." key\n'
110
+ else:
111
+ cmd += f'{self.success_handling}\n'
112
+
113
+ return cmd
114
+
115
+ def _get_log_level_arg(self):
116
+ """Return the correct log level argument for TonieToolbox CLI based on self.log_level."""
117
+ level = str(self.log_level).strip().upper()
118
+ if level == 'DEBUG':
119
+ return '--debug'
120
+ elif level == 'INFO':
121
+ return '--info'
122
+ return '--silent'
123
+
124
+ def _setup_commands(self):
125
+ """Set up all command strings for quick actions dynamically."""
126
+ log_level_arg = self._get_log_level_arg()
127
+
128
+ # Audio file commands
129
+ self.convert_cmd = self._build_cmd(f'{log_level_arg}', log_to_file=self.log_to_file)
130
+ self.upload_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, log_to_file=self.log_to_file)
131
+ self.upload_artwork_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
132
+ 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)
133
+
134
+ # .taf file commands
135
+ self.show_info_cmd = self._build_cmd(log_level_arg, use_info=True, keep_open=True, log_to_file=self.log_to_file)
136
+ self.extract_opus_cmd = self._build_cmd(log_level_arg, is_split=True, log_to_file=self.log_to_file)
137
+ self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
138
+ self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
139
+ 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)
140
+ self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
141
+
142
+ # Folder commands
143
+ self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
144
+ 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)
145
+ 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)
146
+ 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)
147
+
148
+ def _apply_config_template(self):
149
+ """Apply the default configuration template if config.json is missing or invalid."""
150
+ config_path = os.path.join(self.output_dir, 'config.json')
151
+ if not os.path.exists(config_path):
152
+ with open(config_path, 'w') as f:
153
+ json.dump(CONFIG_TEMPLATE, f, indent=4)
154
+ logger.debug(f"Default configuration created at {config_path}")
155
+ return CONFIG_TEMPLATE
156
+ else:
157
+ logger.debug(f"Configuration file found at {config_path}")
158
+ return self._load_config()
159
+
160
+ def _load_config(self):
161
+ """Load configuration settings from config.json"""
162
+ config_path = os.path.join(self.output_dir, 'config.json')
163
+ if not os.path.exists(config_path):
164
+ logger.debug(f"Configuration file not found: {config_path}")
165
+ return {}
166
+
167
+ try:
168
+ with open(config_path, 'r') as f:
169
+ config = json.loads(f.read())
170
+ return config
171
+ except (json.JSONDecodeError, IOError) as e:
172
+ logger.debug(f"Error loading config: {e}")
173
+ return {}
174
+
175
+ def _setup_upload(self):
176
+ """Set up upload functionality based on config.json settings"""
177
+ # Always initialize authentication flags
178
+ self.basic_authentication = False
179
+ self.client_cert_authentication = False
180
+ self.none_authentication = False
181
+
182
+ config = self.config
183
+ try:
184
+ upload_config = config.get('upload', {})
185
+ self.upload_urls = upload_config.get('url', [])
186
+ self.ignore_ssl_verify = upload_config.get('ignore_ssl_verify', False)
187
+ self.username = upload_config.get('username', '')
188
+ self.password = upload_config.get('password', '')
189
+ self.basic_authentication_cmd = ''
190
+ self.client_cert_cmd = ''
191
+
192
+ if self.username and self.password:
193
+ self.basic_authentication_cmd = f'--username {self.username} --password {self.password}'
194
+ self.basic_authentication = True
195
+
196
+ self.client_cert_path = upload_config.get('client_cert_path', '')
197
+ self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
198
+ if self.client_cert_path and self.client_cert_key_path:
199
+ self.client_cert_cmd = f'--client-cert {self.client_cert_path} --client-cert-key {self.client_cert_key_path}'
200
+ self.client_cert_authentication = True
201
+
202
+ if self.client_cert_authentication and self.basic_authentication:
203
+ logger.warning("Both client certificate and basic authentication are set. Only one can be used.")
204
+ return False
205
+
206
+ self.upload_url = self.upload_urls[0] if self.upload_urls else ''
207
+ if not self.client_cert_authentication and not self.basic_authentication and self.upload_url:
208
+ self.none_authentication = True
209
+
210
+ return bool(self.upload_url)
211
+ except Exception as e:
212
+ logger.debug(f"Unexpected error while loading configuration: {e}")
213
+ return False
214
+
215
+ def _create_quick_action(self, name, command, file_types=None, directory_based=False):
216
+ """Create a macOS Quick Action (Service) with the given name and command."""
217
+ # Create Quick Action directory
218
+ action_dir = os.path.join(self.services_dir, f"{name}.workflow")
219
+ os.makedirs(action_dir, exist_ok=True)
220
+
221
+ # Create Contents directory
222
+ contents_dir = os.path.join(action_dir, "Contents")
223
+ os.makedirs(contents_dir, exist_ok=True)
224
+
225
+ # Create document.wflow file with plist content
226
+ document_path = os.path.join(contents_dir, "document.wflow")
227
+
228
+ # Create Info.plist
229
+ info_plist = {
230
+ "NSServices": [
231
+ {
232
+ "NSMenuItem": {
233
+ "default": name
234
+ },
235
+ "NSMessage": "runWorkflowAsService",
236
+ "NSRequiredContext": {
237
+ "NSApplicationIdentifier": "com.apple.finder"
238
+ },
239
+ "NSSendFileTypes": file_types if file_types else [],
240
+ "NSSendTypes": ["NSFilenamesPboardType"] if directory_based else []
241
+ }
242
+ ]
243
+ }
244
+
245
+ info_path = os.path.join(contents_dir, "Info.plist")
246
+ with open(info_path, "wb") as f:
247
+ plistlib.dump(info_plist, f)
248
+
249
+ # Create script file
250
+ script_dir = os.path.join(contents_dir, "MacOS")
251
+ os.makedirs(script_dir, exist_ok=True)
252
+ script_path = os.path.join(script_dir, "script")
253
+
254
+ with open(script_path, "w") as f:
255
+ f.write(command)
256
+
257
+ # Make the script executable
258
+ os.chmod(script_path, 0o755)
259
+
260
+ # Create document.wflow file with a basic workflow definition
261
+ workflow = {
262
+ "AMApplication": "Automator",
263
+ "AMCanShowSelectedItemsWhenRun": False,
264
+ "AMCanShowWhenRun": True,
265
+ "AMDockBadgeLabel": "",
266
+ "AMDockBadgeStyle": "badge",
267
+ "AMName": name,
268
+ "AMRootElement": {
269
+ "actions": [
270
+ {
271
+ "action": "run-shell-script",
272
+ "parameters": {
273
+ "shell": "/bin/bash",
274
+ "script": command,
275
+ "input": "as arguments"
276
+ }
277
+ }
278
+ ],
279
+ "class": "workflow",
280
+ "connections": {},
281
+ "id": "workflow-element",
282
+ "title": name
283
+ },
284
+ "AMWorkflowSchemeVersion": 2.0,
285
+ }
286
+
287
+ with open(document_path, "wb") as f:
288
+ plistlib.dump(workflow, f)
289
+
290
+ return action_dir
291
+
292
+ def _generate_audio_extension_actions(self):
293
+ """Generate Quick Actions for supported audio file extensions."""
294
+ extensions = [ext.lower().lstrip('.') for ext in SUPPORTED_EXTENSIONS]
295
+
296
+ # Create audio file actions
297
+ self._create_quick_action(
298
+ "TonieToolbox - Convert to TAF",
299
+ self.convert_cmd,
300
+ file_types=extensions
301
+ )
302
+
303
+ if self.upload_enabled:
304
+ self._create_quick_action(
305
+ "TonieToolbox - Convert and Upload",
306
+ self.upload_cmd,
307
+ file_types=extensions
308
+ )
309
+
310
+ self._create_quick_action(
311
+ "TonieToolbox - Convert, Upload with Artwork",
312
+ self.upload_artwork_cmd,
313
+ file_types=extensions
314
+ )
315
+
316
+ self._create_quick_action(
317
+ "TonieToolbox - Convert, Upload with Artwork and JSON",
318
+ self.upload_artwork_json_cmd,
319
+ file_types=extensions
320
+ )
321
+
322
+ def _generate_taf_file_actions(self):
323
+ """Generate Quick Actions for .taf files."""
324
+ self._create_quick_action(
325
+ "TonieToolbox - Show Info",
326
+ self.show_info_cmd,
327
+ file_types=["taf"]
328
+ )
329
+
330
+ self._create_quick_action(
331
+ "TonieToolbox - Extract Opus Tracks",
332
+ self.extract_opus_cmd,
333
+ file_types=["taf"]
334
+ )
335
+
336
+ if self.upload_enabled:
337
+ self._create_quick_action(
338
+ "TonieToolbox - Upload",
339
+ self.upload_taf_cmd,
340
+ file_types=["taf"]
341
+ )
342
+
343
+ self._create_quick_action(
344
+ "TonieToolbox - Upload with Artwork",
345
+ self.upload_taf_artwork_cmd,
346
+ file_types=["taf"]
347
+ )
348
+
349
+ self._create_quick_action(
350
+ "TonieToolbox - Upload with Artwork and JSON",
351
+ self.upload_taf_artwork_json_cmd,
352
+ file_types=["taf"]
353
+ )
354
+
355
+ self._create_quick_action(
356
+ "TonieToolbox - Compare with another TAF file",
357
+ self.compare_taf_cmd,
358
+ file_types=["taf"]
359
+ )
360
+
361
+ def _generate_folder_actions(self):
362
+ """Generate Quick Actions for folders."""
363
+ self._create_quick_action(
364
+ "TonieToolbox - Convert Folder to TAF (recursive)",
365
+ self.convert_folder_cmd,
366
+ directory_based=True
367
+ )
368
+
369
+ if self.upload_enabled:
370
+ self._create_quick_action(
371
+ "TonieToolbox - Convert Folder and Upload (recursive)",
372
+ self.upload_folder_cmd,
373
+ directory_based=True
374
+ )
375
+
376
+ self._create_quick_action(
377
+ "TonieToolbox - Convert Folder, Upload with Artwork (recursive)",
378
+ self.upload_folder_artwork_cmd,
379
+ directory_based=True
380
+ )
381
+
382
+ self._create_quick_action(
383
+ "TonieToolbox - Convert Folder, Upload with Artwork and JSON (recursive)",
384
+ self.upload_folder_artwork_json_cmd,
385
+ directory_based=True
386
+ )
387
+
388
+ def install_quick_actions(self):
389
+ """
390
+ Install all Quick Actions.
391
+
392
+ Returns:
393
+ bool: True if all actions were installed successfully, False otherwise.
394
+ """
395
+ try:
396
+ # Ensure Services directory exists
397
+ os.makedirs(self.services_dir, exist_ok=True)
398
+
399
+ # Check if the icon exists, copy default if needed
400
+ if not os.path.exists(self.icon_path):
401
+ # Include code to extract icon from resources
402
+ logger.debug(f"Icon not found at {self.icon_path}, using default")
403
+
404
+ # Generate Quick Actions for different file types
405
+ self._generate_audio_extension_actions()
406
+ self._generate_taf_file_actions()
407
+ self._generate_folder_actions()
408
+
409
+ # Refresh the Services menu by restarting the Finder
410
+ result = subprocess.run(["killall", "-HUP", "Finder"], check=False,
411
+ capture_output=True, text=True)
412
+
413
+ print("TonieToolbox Quick Actions installed successfully.")
414
+ print("You'll find them in the Services menu when right-clicking on audio files, TAF files, or folders.")
415
+
416
+ return True
417
+ except Exception as e:
418
+ logger.error(f"Failed to install Quick Actions: {e}")
419
+ return False
420
+
421
+ def uninstall_quick_actions(self):
422
+ """
423
+ Uninstall all TonieToolbox Quick Actions.
424
+
425
+ Returns:
426
+ bool: True if all actions were uninstalled successfully, False otherwise.
427
+ """
428
+ try:
429
+ any_failures = False
430
+ for item in os.listdir(self.services_dir):
431
+ if item.startswith("TonieToolbox - ") and item.endswith(".workflow"):
432
+ action_path = os.path.join(self.services_dir, item)
433
+ try:
434
+ subprocess.run(["rm", "-rf", action_path], check=True)
435
+ print(f"Removed: {item}")
436
+ except subprocess.CalledProcessError as e:
437
+ print(f"Failed to remove: {item}")
438
+ logger.error(f"Error removing {item}: {e}")
439
+ any_failures = True
440
+ subprocess.run(["killall", "-HUP", "Finder"], check=False)
441
+
442
+ print("TonieToolbox Quick Actions uninstalled successfully.")
443
+
444
+ return not any_failures
445
+ except Exception as e:
446
+ logger.error(f"Failed to uninstall Quick Actions: {e}")
447
+ return False @classmethod
448
+ def install(cls):
449
+ """
450
+ Generate Quick Actions and install them.
451
+
452
+ Returns:
453
+ bool: True if installation was successful, False otherwise.
454
+ """
455
+ instance = cls()
456
+ if instance.install_quick_actions():
457
+ logger.info("macOS integration installed successfully.")
458
+ return True
459
+ else:
460
+ logger.error("macOS integration installation failed.")
461
+ return False
462
+
463
+ @classmethod
464
+ def uninstall(cls):
465
+ """
466
+ Uninstall all TonieToolbox Quick Actions.
467
+
468
+ Returns:
469
+ bool: True if uninstallation was successful, False otherwise.
470
+ """
471
+ instance = cls()
472
+ if instance.uninstall_quick_actions():
473
+ logger.info("macOS integration uninstalled successfully.")
474
+ return True
475
+ else:
476
+ logger.error("macOS integration uninstallation failed.")
477
+ return False
@@ -0,0 +1 @@
1
+ # TODO: Add integration_ubuntu.py