TonieToolbox 0.6.0a1__py3-none-any.whl → 0.6.0a3__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 CHANGED
@@ -2,4 +2,4 @@
2
2
  TonieToolbox - Convert audio files to Tonie box compatible format
3
3
  """
4
4
 
5
- __version__ = '0.6.0a1'
5
+ __version__ = '0.6.0a3'
TonieToolbox/__main__.py CHANGED
@@ -21,7 +21,7 @@ from .teddycloud import TeddyCloudClient
21
21
  from .tags import get_tags
22
22
  from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
23
23
  from .artwork import upload_artwork
24
- from .integration import handle_integration
24
+ from .integration import handle_integration, handle_config
25
25
 
26
26
  def main():
27
27
  """Entry point for the TonieToolbox application."""
@@ -101,6 +101,8 @@ def main():
101
101
  parser.add_argument('-D', '--detailed-compare', action='store_true',
102
102
  help='Show detailed OGG page differences when comparing files')
103
103
  # ------------- Parser - Context Menu Integration -------------
104
+ parser.add_argument('--config-integration', action='store_true',
105
+ help='Configure context menu integration')
104
106
  parser.add_argument('--install-integration', action='store_true',
105
107
  help='Integrate with the system (e.g., create context menu entries)')
106
108
  parser.add_argument('--uninstall-integration', action='store_true',
@@ -133,7 +135,7 @@ def main():
133
135
  args = parser.parse_args()
134
136
 
135
137
  # ------------- Parser - Source Input -------------
136
- if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration):
138
+ if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration or args.config_integration):
137
139
  parser.error("the following arguments are required: SOURCE")
138
140
 
139
141
  # ------------- Logging -------------
@@ -185,6 +187,10 @@ def main():
185
187
  else:
186
188
  logger.error("Failed to handle context menu integration")
187
189
  sys.exit(0)
190
+ if args.config_integration:
191
+ logger.debug("Opening configuration file for editing")
192
+ handle_config()
193
+ sys.exit(0)
188
194
  # ------------- Normalize Path Input -------------
189
195
  if args.input_filename:
190
196
  logger.debug("Original input path: %s", args.input_filename)
TonieToolbox/constants.py CHANGED
@@ -95,6 +95,24 @@ SUPPORTED_EXTENSIONS = [
95
95
  '.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
96
96
  ]
97
97
 
98
+ UTI_MAPPINGS = {
99
+ 'mp3': 'public.mp3',
100
+ 'wav': 'public.wav',
101
+ 'flac': 'org.xiph.flac',
102
+ 'ogg': 'org.xiph.ogg',
103
+ 'opus': 'public.opus',
104
+ 'aac': 'public.aac-audio',
105
+ 'm4a': 'public.m4a-audio',
106
+ 'wma': 'com.microsoft.windows-media-wma',
107
+ 'aiff': 'public.aiff-audio',
108
+ 'mp2': 'public.mp2',
109
+ 'mp4': 'public.mpeg-4-audio',
110
+ 'mka': 'public.audio',
111
+ 'webm': 'public.webm-audio',
112
+ 'ape': 'public.audio',
113
+ 'taf': 'public.audio'
114
+ }
115
+
98
116
  ARTWORK_NAMES = [
99
117
  'cover', 'folder', 'album', 'front', 'artwork', 'image',
100
118
  'albumart', 'albumartwork', 'booklet'
@@ -171,4 +189,21 @@ TAG_MAPPINGS = {
171
189
  'albuminterpret': 'albumartist', # German tag name
172
190
  'interpret': 'artist', # German tag name
173
191
 
192
+ }
193
+
194
+ CONFIG_TEMPLATE = {
195
+ "metadata": {
196
+ "description": "TonieToolbox configuration",
197
+ "config_version": "1.0"
198
+ },
199
+ "log_level": "silent", # Options: trace, debug, info, warning, error, critical, silent
200
+ "log_to_file": False, # True if you want to log to a file ~\.tonietoolbox\logs
201
+ "upload": {
202
+ "url": [""], # https://teddycloud.example.com
203
+ "ignore_ssl_verify": False, # True if you want to ignore SSL certificate verification
204
+ "username": "", # Basic Auth username
205
+ "password": "", # Basic Auth password
206
+ "client_cert_path": "", # Path to client certificate file
207
+ "client_cert_key_path": "" # Path to client certificate key file
208
+ }
174
209
  }
@@ -35,7 +35,7 @@ DEPENDENCIES = {
35
35
  'extract_dir': 'ffmpeg'
36
36
  },
37
37
  'darwin': {
38
- 'url': 'https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip',
38
+ 'url': 'https://evermeet.cx/ffmpeg/get/zip',
39
39
  'bin_path': 'ffmpeg',
40
40
  'extract_dir': 'ffmpeg'
41
41
  }
@@ -112,6 +112,11 @@ def download_file(url, destination):
112
112
  return True
113
113
  except Exception as e:
114
114
  logger.error("Failed to download %s: %s", url, e)
115
+ # On macOS, provide more helpful error message for SSL certificate issues
116
+ if platform.system() == 'Darwin' and 'CERTIFICATE_VERIFY_FAILED' in str(e):
117
+ logger.error("SSL certificate verification failed on macOS. This is a known issue.")
118
+ logger.error("You can solve this by running: /Applications/Python 3.x/Install Certificates.command")
119
+ logger.error("Or by using the --auto-download flag which will bypass certificate verification.")
115
120
  return False
116
121
 
117
122
  def extract_archive(archive_path, extract_dir):
@@ -1,20 +1,68 @@
1
1
  import platform
2
+ from .logger import get_logger
3
+
4
+ logger = get_logger(__name__)
2
5
 
3
6
  def handle_integration(args):
4
7
  import platform
5
8
  if platform.system() == 'Windows':
6
9
  from .integration_windows import WindowsClassicContextMenuIntegration as ContextMenuIntegration
7
10
  if args.install_integration:
8
- ContextMenuIntegration.install()
11
+ success = ContextMenuIntegration.install()
12
+ if success:
13
+ logger.info("Integration installed successfully.")
14
+ return True
15
+ else:
16
+ logger.error("Integration installation failed.")
17
+ return False
9
18
  elif args.uninstall_integration:
10
- ContextMenuIntegration.uninstall()
19
+ success = ContextMenuIntegration.uninstall()
20
+ if success:
21
+ logger.info("Integration uninstalled successfully.")
22
+ return True
23
+ else:
24
+ logger.error("Integration uninstallation failed.")
25
+ return False
11
26
  elif platform.system() == 'Darwin':
12
27
  from .integration_macos import MacOSContextMenuIntegration as ContextMenuIntegration
13
28
  if args.install_integration:
14
- ContextMenuIntegration.install()
29
+ success = ContextMenuIntegration.install()
30
+ if success:
31
+ logger.info("Integration installed successfully.")
32
+ return True
33
+ else:
34
+ logger.error("Integration installation failed.")
35
+ return False
15
36
  elif args.uninstall_integration:
16
- ContextMenuIntegration.uninstall()
37
+ success = ContextMenuIntegration.uninstall()
38
+ if success:
39
+ logger.info("Integration uninstalled successfully.")
40
+ return True
41
+ else:
42
+ logger.error("Integration uninstallation failed.")
43
+ return False
17
44
  elif platform.system() == 'Linux':
18
45
  raise NotImplementedError("Context menu integration is not supported on Linux YET. But Soon™")
19
46
  else:
20
- raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
47
+ raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
48
+
49
+ def handle_config():
50
+ """Opens the configuration file in the default text editor."""
51
+ import os
52
+ import platform
53
+ import subprocess
54
+
55
+ config_path = os.path.join(os.path.expanduser("~"), ".tonietoolbox", "config.json")
56
+
57
+ if not os.path.exists(config_path):
58
+ logger.info(f"Configuration file not found at {config_path}.")
59
+ logger.info("Creating a new configuration file. Using --install-integration will create a new config file.")
60
+ return
61
+ if platform.system() == "Windows":
62
+ os.startfile(config_path)
63
+ elif platform.system() == "Darwin":
64
+ subprocess.call(["open", config_path])
65
+ elif platform.system() == "Linux":
66
+ subprocess.call(["xdg-open", config_path])
67
+ else:
68
+ logger.error(f"Unsupported OS: {platform.system()}")
@@ -1,11 +1,10 @@
1
- # filepath: d:\Repository\TonieToolbox\TonieToolbox\integration_macos.py
2
1
  import os
3
2
  import sys
4
3
  import json
5
4
  import plistlib
6
5
  import subprocess
7
6
  from pathlib import Path
8
- from .constants import SUPPORTED_EXTENSIONS
7
+ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE,UTI_MAPPINGS
9
8
  from .logger import get_logger
10
9
 
11
10
  logger = get_logger('integration_macos')
@@ -22,15 +21,9 @@ class MacOSContextMenuIntegration:
22
21
  self.services_dir = os.path.join(os.path.expanduser('~'), 'Library', 'Services')
23
22
  self.icon_path = os.path.join(self.output_dir, 'icon.png')
24
23
  os.makedirs(self.output_dir, exist_ok=True)
25
-
26
- # Error handling and success messages for shell scripts
27
24
  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
25
  self.success_handling = 'echo "Command completed successfully"\nsleep 2'
29
-
30
- # Load configuration
31
- self.config = self._load_config()
32
-
33
- # Ensure these attributes always exist
26
+ self.config = self._apply_config_template()
34
27
  self.upload_url = ''
35
28
  self.log_level = self.config.get('log_level', 'SILENT')
36
29
  self.log_to_file = self.config.get('log_to_file', False)
@@ -40,14 +33,19 @@ class MacOSContextMenuIntegration:
40
33
 
41
34
  logger.debug(f"Upload enabled: {self.upload_enabled}")
42
35
  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
-
36
+ 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'))}")
37
+ self._setup_commands()
38
+
47
39
  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
40
  """Dynamically build command strings for quick actions."""
49
41
  exe = self.exe_path
50
42
  cmd = '#!/bin/bash\n\n'
43
+ # Debug output to see what's being passed to the script
44
+ cmd += 'echo "Arguments received: $@"\n'
45
+ cmd += 'echo "Number of arguments: $#"\n'
46
+ cmd += 'if [ $# -gt 0 ]; then\n'
47
+ cmd += ' echo "First argument: $1"\n'
48
+ cmd += 'fi\n\n'
51
49
 
52
50
  # Add a description of what's being executed
53
51
  cmd += 'echo "Running TonieToolbox'
@@ -70,12 +68,117 @@ class MacOSContextMenuIntegration:
70
68
  else:
71
69
  cmd += ' convert'
72
70
  cmd += ' command..."\n\n'
71
+ # Properly handle paths from macOS Services
72
+ if is_folder or is_recursive:
73
+ # Handle multiple arguments and ensure we get a valid folder
74
+ cmd += '# Handle paths from macOS Services\n'
75
+ cmd += '# First, try to get paths from stdin (macOS passes paths this way)\n'
76
+ cmd += 'if [ -p /dev/stdin ]; then\n'
77
+ cmd += ' PATHS=$(cat /dev/stdin)\n'
78
+ cmd += ' echo "Found paths from stdin: $PATHS"\n'
79
+ cmd += 'fi\n\n'
80
+ cmd += '# If no paths from stdin, check command line arguments\n'
81
+ cmd += 'FOLDER_PATH=""\n'
82
+ cmd += 'if [ -z "$PATHS" ]; then\n'
83
+ cmd += ' for arg in "$@"; do\n'
84
+ cmd += ' if [ -d "$arg" ]; then\n'
85
+ cmd += ' FOLDER_PATH="$arg"\n'
86
+ cmd += ' echo "Processing folder from args: $FOLDER_PATH"\n'
87
+ cmd += ' break\n'
88
+ cmd += ' fi\n'
89
+ cmd += ' done\n'
90
+ cmd += 'else\n'
91
+ cmd += ' for path in $PATHS; do\n'
92
+ cmd += ' if [ -d "$path" ]; then\n'
93
+ cmd += ' FOLDER_PATH="$path"\n'
94
+ cmd += ' echo "Processing folder from stdin: $FOLDER_PATH"\n'
95
+ cmd += ' break\n'
96
+ cmd += ' fi\n'
97
+ cmd += ' done\n'
98
+ cmd += 'fi\n\n'
99
+ cmd += 'if [ -z "$FOLDER_PATH" ]; then\n'
100
+ cmd += ' echo "Error: No valid folder path found in arguments or stdin"\n'
101
+ cmd += ' read -p "Press any key to close this window..." key\n'
102
+ cmd += ' exit 1\n'
103
+ cmd += 'fi\n\n'
104
+
105
+ # Use the variable for the command
106
+ file_placeholder='$FOLDER_PATH'
107
+ elif use_compare:
108
+ # For compare operation, we need two file paths
109
+ cmd += '# Compare requires two files\n'
110
+ cmd += 'if [ $# -lt 2 ]; then\n'
111
+ cmd += ' echo "Error: Compare operation requires two files."\n'
112
+ cmd += ' read -p "Press any key to close this window..." key\n'
113
+ cmd += ' exit 1\n'
114
+ cmd += 'fi\n\n'
115
+ else:
116
+ # For regular file operations, handle paths correctly
117
+ cmd += '# Handle file paths correctly - try multiple methods for macOS\n'
118
+ cmd += 'FILE_PATH=""\n'
119
+
120
+ # First, try to get paths from stdin (macOS passes paths this way sometimes)
121
+ cmd += '# Method 1: Try to read from stdin if available\n'
122
+ cmd += 'if [ -p /dev/stdin ]; then\n'
123
+ cmd += ' STDIN_PATHS=$(cat)\n'
124
+ cmd += ' if [ -n "$STDIN_PATHS" ]; then\n'
125
+ cmd += ' for path in $STDIN_PATHS; do\n'
126
+ cmd += ' if [ -f "$path" ]; then\n'
127
+ cmd += ' FILE_PATH="$path"\n'
128
+ cmd += ' echo "Found file path from stdin: $FILE_PATH"\n'
129
+ cmd += ' break\n'
130
+ cmd += ' fi\n'
131
+ cmd += ' done\n'
132
+ cmd += ' fi\n'
133
+ cmd += 'fi\n\n'
134
+
135
+ # Method 2: Try command line arguments
136
+ cmd += '# Method 2: Check command line arguments\n'
137
+ cmd += 'if [ -z "$FILE_PATH" ]; then\n'
138
+ cmd += ' for arg in "$@"; do\n'
139
+ cmd += ' if [ -f "$arg" ]; then\n'
140
+ cmd += ' FILE_PATH="$arg"\n'
141
+ cmd += ' echo "Found file path from arguments: $FILE_PATH"\n'
142
+ cmd += ' break\n'
143
+ cmd += ' fi\n'
144
+ cmd += ' done\n'
145
+ cmd += 'fi\n\n'
146
+
147
+ # Method 3: Try to handle case where path might be in $1
148
+ cmd += '# Method 3: Try first argument directly\n'
149
+ cmd += 'if [ -z "$FILE_PATH" ] && [ -n "$1" ] && [ -f "$1" ]; then\n'
150
+ cmd += ' FILE_PATH="$1"\n'
151
+ cmd += ' echo "Using first argument directly as file path: $FILE_PATH"\n'
152
+ cmd += 'fi\n\n'
153
+
154
+ # Method 4: Parse automator's encoded path format
155
+ cmd += '# Method 4: Try to decode special format macOS might use\n'
156
+ cmd += 'if [ -z "$FILE_PATH" ] && [ -n "$1" ]; then\n'
157
+ cmd += ' # Sometimes macOS passes paths with "file://" prefix\n'
158
+ cmd += ' DECODED_PATH=$(echo "$1" | sed -e "s|^file://||" -e "s|%20| |g")\n'
159
+ cmd += ' if [ -f "$DECODED_PATH" ]; then\n'
160
+ cmd += ' FILE_PATH="$DECODED_PATH"\n'
161
+ cmd += ' echo "Using decoded path: $FILE_PATH"\n'
162
+ cmd += ' fi\n'
163
+ cmd += 'fi\n\n'
164
+
165
+ # Final check
166
+ cmd += 'if [ -z "$FILE_PATH" ]; then\n'
167
+ cmd += ' echo "Error: Could not find a valid file path. Tried:"\n'
168
+ cmd += ' echo "- Reading from stdin"\n'
169
+ cmd += ' echo "- Command arguments: $@"\n'
170
+ cmd += ' echo "- Decoding URL format"\n'
171
+ cmd += ' read -p "Press any key to close this window..." key\n'
172
+ cmd += ' exit 1\n'
173
+ cmd += 'fi\n\n'
174
+
175
+ # Use the variable for the command
176
+ file_placeholder='$FILE_PATH'
73
177
 
74
178
  # Build the actual command
75
179
  cmd_line = f'"{exe}" {base_args}'
76
180
  if log_to_file:
77
- cmd_line += ' --log-file'
78
- if is_recursive:
181
+ cmd_line += ' --log-file' if is_recursive:
79
182
  cmd_line += ' --recursive'
80
183
  if output_to_source:
81
184
  cmd_line += ' --output-to-source'
@@ -84,7 +187,28 @@ class MacOSContextMenuIntegration:
84
187
  if is_split:
85
188
  cmd_line += ' --split'
86
189
  if use_compare:
87
- cmd_line += ' --compare "$1" "$2"'
190
+ # For compare, we need to handle two files
191
+ cmd += '# Find two TAF files for comparison\n'
192
+ cmd += 'FILE1=""\n'
193
+ cmd += 'FILE2=""\n'
194
+ cmd += 'for arg in "$@"; do\n'
195
+ cmd += ' if [ -f "$arg" ]; then\n'
196
+ cmd += ' if [ -z "$FILE1" ]; then\n'
197
+ cmd += ' FILE1="$arg"\n'
198
+ cmd += ' echo "First TAF file: $FILE1"\n'
199
+ cmd += ' elif [ -z "$FILE2" ]; then\n'
200
+ cmd += ' FILE2="$arg"\n'
201
+ cmd += ' echo "Second TAF file: $FILE2"\n'
202
+ cmd += ' break\n'
203
+ cmd += ' fi\n'
204
+ cmd += ' fi\n'
205
+ cmd += 'done\n\n'
206
+ cmd += 'if [ -z "$FILE1" ] || [ -z "$FILE2" ]; then\n'
207
+ cmd += ' echo "Error: Need two TAF files for comparison."\n'
208
+ cmd += ' read -p "Press any key to close this window..." key\n'
209
+ cmd += ' exit 1\n'
210
+ cmd += 'fi\n\n'
211
+ cmd_line += ' --compare "$FILE1" "$FILE2"'
88
212
  else:
89
213
  cmd_line += f' "{file_placeholder}"'
90
214
  if use_upload:
@@ -101,6 +225,7 @@ class MacOSContextMenuIntegration:
101
225
  cmd_line += ' --create-custom-json'
102
226
 
103
227
  # Add the command to the script
228
+ cmd += f'echo "Executing: {cmd_line}"\n'
104
229
  cmd += f'{cmd_line}\n\n'
105
230
 
106
231
  # Add error and success handling
@@ -145,6 +270,18 @@ class MacOSContextMenuIntegration:
145
270
  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
271
  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
272
 
273
+ def _apply_config_template(self):
274
+ """Apply the default configuration template if config.json is missing or invalid."""
275
+ config_path = os.path.join(self.output_dir, 'config.json')
276
+ if not os.path.exists(config_path):
277
+ with open(config_path, 'w') as f:
278
+ json.dump(CONFIG_TEMPLATE, f, indent=4)
279
+ logger.debug(f"Default configuration created at {config_path}")
280
+ return CONFIG_TEMPLATE
281
+ else:
282
+ logger.debug(f"Configuration file found at {config_path}")
283
+ return self._load_config()
284
+
148
285
  def _load_config(self):
149
286
  """Load configuration settings from config.json"""
150
287
  config_path = os.path.join(self.output_dir, 'config.json')
@@ -162,7 +299,6 @@ class MacOSContextMenuIntegration:
162
299
 
163
300
  def _setup_upload(self):
164
301
  """Set up upload functionality based on config.json settings"""
165
- # Always initialize authentication flags
166
302
  self.basic_authentication = False
167
303
  self.client_cert_authentication = False
168
304
  self.none_authentication = False
@@ -198,22 +334,16 @@ class MacOSContextMenuIntegration:
198
334
  return bool(self.upload_url)
199
335
  except Exception as e:
200
336
  logger.debug(f"Unexpected error while loading configuration: {e}")
201
- return False
202
-
337
+ return False
203
338
  def _create_quick_action(self, name, command, file_types=None, directory_based=False):
204
339
  """Create a macOS Quick Action (Service) with the given name and command."""
205
- # Create Quick Action directory
206
340
  action_dir = os.path.join(self.services_dir, f"{name}.workflow")
207
341
  os.makedirs(action_dir, exist_ok=True)
208
-
209
- # Create Contents directory
210
342
  contents_dir = os.path.join(action_dir, "Contents")
211
343
  os.makedirs(contents_dir, exist_ok=True)
212
-
213
- # Create document.wflow file with plist content
214
344
  document_path = os.path.join(contents_dir, "document.wflow")
215
345
 
216
- # Create Info.plist
346
+ # Set up the plist to ensure the service appears in context menus
217
347
  info_plist = {
218
348
  "NSServices": [
219
349
  {
@@ -225,30 +355,27 @@ class MacOSContextMenuIntegration:
225
355
  "NSApplicationIdentifier": "com.apple.finder"
226
356
  },
227
357
  "NSSendFileTypes": file_types if file_types else [],
228
- "NSSendTypes": ["NSFilenamesPboardType"] if directory_based else []
358
+ "NSSendTypes": ["NSFilenamesPboardType"], # Always include this to ensure paths are passed correctly
359
+ "NSUserData": name,
360
+ "NSExecutable": "script", # Ensure macOS knows which script to run
361
+ "NSReturnTypes": []
229
362
  }
230
363
  ]
231
364
  }
232
365
 
233
366
  info_path = os.path.join(contents_dir, "Info.plist")
234
367
  with open(info_path, "wb") as f:
235
- plistlib.dump(info_plist, f)
236
-
237
- # Create script file
368
+ plistlib.dump(info_plist, f)
238
369
  script_dir = os.path.join(contents_dir, "MacOS")
239
370
  os.makedirs(script_dir, exist_ok=True)
240
371
  script_path = os.path.join(script_dir, "script")
241
372
 
242
373
  with open(script_path, "w") as f:
243
- f.write(command)
244
-
245
- # Make the script executable
374
+ f.write(command)
246
375
  os.chmod(script_path, 0o755)
247
-
248
- # Create document.wflow file with a basic workflow definition
249
376
  workflow = {
250
377
  "AMApplication": "Automator",
251
- "AMCanShowSelectedItemsWhenRun": False,
378
+ "AMCanShowSelectedItemsWhenRun": True,
252
379
  "AMCanShowWhenRun": True,
253
380
  "AMDockBadgeLabel": "",
254
381
  "AMDockBadgeStyle": "badge",
@@ -260,7 +387,11 @@ class MacOSContextMenuIntegration:
260
387
  "parameters": {
261
388
  "shell": "/bin/bash",
262
389
  "script": command,
263
- "input": "as arguments"
390
+ "input": "as arguments",
391
+ "showStdout": True,
392
+ "showStderr": True,
393
+ "showOutput": True,
394
+ "runAsAdmin": False
264
395
  }
265
396
  }
266
397
  ],
@@ -271,158 +402,202 @@ class MacOSContextMenuIntegration:
271
402
  },
272
403
  "AMWorkflowSchemeVersion": 2.0,
273
404
  }
274
-
275
405
  with open(document_path, "wb") as f:
276
406
  plistlib.dump(workflow, f)
277
407
 
278
408
  return action_dir
279
-
409
+
410
+ def _extension_to_uti(self, extension):
411
+ """Convert a file extension to macOS UTI (Uniform Type Identifier)."""
412
+ uti_map = UTI_MAPPINGS
413
+ ext = extension.lower().lstrip('.')
414
+ return uti_map.get(ext, f'public.{ext}')
415
+
280
416
  def _generate_audio_extension_actions(self):
281
417
  """Generate Quick Actions for supported audio file extensions."""
282
418
  extensions = [ext.lower().lstrip('.') for ext in SUPPORTED_EXTENSIONS]
283
-
284
- # Create audio file actions
419
+ # Convert extensions to UTIs (Uniform Type Identifiers)
420
+ utis = [self._extension_to_uti(ext) for ext in extensions]
285
421
  self._create_quick_action(
286
422
  "TonieToolbox - Convert to TAF",
287
423
  self.convert_cmd,
288
- file_types=extensions
424
+ file_types=utis
289
425
  )
290
426
 
291
427
  if self.upload_enabled:
292
428
  self._create_quick_action(
293
429
  "TonieToolbox - Convert and Upload",
294
430
  self.upload_cmd,
295
- file_types=extensions
431
+ file_types=utis
296
432
  )
297
433
 
298
434
  self._create_quick_action(
299
435
  "TonieToolbox - Convert, Upload with Artwork",
300
436
  self.upload_artwork_cmd,
301
- file_types=extensions
437
+ file_types=utis
302
438
  )
303
439
 
304
440
  self._create_quick_action(
305
441
  "TonieToolbox - Convert, Upload with Artwork and JSON",
306
442
  self.upload_artwork_json_cmd,
307
- file_types=extensions
443
+ file_types=utis
308
444
  )
309
-
445
+
310
446
  def _generate_taf_file_actions(self):
311
447
  """Generate Quick Actions for .taf files."""
448
+ taf_uti = self._extension_to_uti("taf") # Use UTI for TAF files
449
+
312
450
  self._create_quick_action(
313
451
  "TonieToolbox - Show Info",
314
452
  self.show_info_cmd,
315
- file_types=["taf"]
453
+ file_types=[taf_uti]
316
454
  )
317
455
 
318
456
  self._create_quick_action(
319
457
  "TonieToolbox - Extract Opus Tracks",
320
458
  self.extract_opus_cmd,
321
- file_types=["taf"]
459
+ file_types=[taf_uti]
322
460
  )
323
461
 
324
462
  if self.upload_enabled:
325
463
  self._create_quick_action(
326
464
  "TonieToolbox - Upload",
327
465
  self.upload_taf_cmd,
328
- file_types=["taf"]
466
+ file_types=[taf_uti]
329
467
  )
330
-
331
468
  self._create_quick_action(
332
469
  "TonieToolbox - Upload with Artwork",
333
470
  self.upload_taf_artwork_cmd,
334
- file_types=["taf"]
471
+ file_types=[taf_uti]
335
472
  )
336
473
 
337
474
  self._create_quick_action(
338
475
  "TonieToolbox - Upload with Artwork and JSON",
339
476
  self.upload_taf_artwork_json_cmd,
340
- file_types=["taf"]
477
+ file_types=[taf_uti]
478
+ )
479
+
480
+ self._create_quick_action(
481
+ "TonieToolbox - Compare with another TAF file",
482
+ self.compare_taf_cmd,
483
+ file_types=[taf_uti]
341
484
  )
342
485
 
343
- self._create_quick_action(
344
- "TonieToolbox - Compare with another TAF file",
345
- self.compare_taf_cmd,
346
- file_types=["taf"]
347
- )
348
-
349
486
  def _generate_folder_actions(self):
350
487
  """Generate Quick Actions for folders."""
351
488
  self._create_quick_action(
352
- "TonieToolbox - Convert Folder to TAF (recursive)",
489
+ "TonieToolbox - 1. Convert Folder to TAF (recursive)",
353
490
  self.convert_folder_cmd,
354
491
  directory_based=True
355
492
  )
356
493
 
357
494
  if self.upload_enabled:
358
495
  self._create_quick_action(
359
- "TonieToolbox - Convert Folder and Upload (recursive)",
496
+ "TonieToolbox - 2. Convert Folder and Upload (recursive)",
360
497
  self.upload_folder_cmd,
361
498
  directory_based=True
362
499
  )
363
500
 
364
501
  self._create_quick_action(
365
- "TonieToolbox - Convert Folder, Upload with Artwork (recursive)",
502
+ "TonieToolbox - 3. Convert Folder, Upload with Artwork (recursive)",
366
503
  self.upload_folder_artwork_cmd,
367
504
  directory_based=True
368
505
  )
369
506
 
370
507
  self._create_quick_action(
371
- "TonieToolbox - Convert Folder, Upload with Artwork and JSON (recursive)",
508
+ "TonieToolbox - 4. Convert Folder, Upload with Artwork and JSON (recursive)",
372
509
  self.upload_folder_artwork_json_cmd,
373
510
  directory_based=True
374
511
  )
375
-
512
+
376
513
  def install_quick_actions(self):
377
- """Install all Quick Actions."""
378
- # Ensure Services directory exists
379
- os.makedirs(self.services_dir, exist_ok=True)
380
-
381
- # Check if the icon exists, copy default if needed
382
- if not os.path.exists(self.icon_path):
383
- # Include code to extract icon from resources
384
- logger.debug(f"Icon not found at {self.icon_path}, using default")
385
-
386
- # Generate Quick Actions for different file types
387
- self._generate_audio_extension_actions()
388
- self._generate_taf_file_actions()
389
- self._generate_folder_actions()
390
-
391
- # Refresh the Services menu by restarting the Finder
392
- subprocess.run(["killall", "-HUP", "Finder"], check=False)
514
+ """
515
+ Install all Quick Actions.
393
516
 
394
- print("TonieToolbox Quick Actions installed successfully.")
395
- print("You'll find them in the Services menu when right-clicking on audio files, TAF files, or folders.")
396
-
517
+ Returns:
518
+ bool: True if all actions were installed successfully, False otherwise.
519
+ """
520
+ try:
521
+ # Ensure Services directory exists
522
+ os.makedirs(self.services_dir, exist_ok=True)
523
+
524
+ # Check if the icon exists, copy default if needed
525
+ if not os.path.exists(self.icon_path):
526
+ # Include code to extract icon from resources
527
+ logger.debug(f"Icon not found at {self.icon_path}, using default")
528
+
529
+ # Generate Quick Actions for different file types
530
+ self._generate_audio_extension_actions()
531
+ self._generate_taf_file_actions()
532
+ self._generate_folder_actions()
533
+
534
+ # Refresh the Services menu by restarting the Finder
535
+ result = subprocess.run(["killall", "-HUP", "Finder"], check=False,
536
+ capture_output=True, text=True)
537
+ logger.info("TonieToolbox Quick Actions installed successfully.")
538
+ logger.info("You'll find them in the Services menu when right-clicking on audio files, TAF files, or folders.")
539
+
540
+ return True
541
+ except Exception as e:
542
+ logger.error(f"Failed to install Quick Actions: {e}")
543
+ return False
544
+
397
545
  def uninstall_quick_actions(self):
398
- """Uninstall all TonieToolbox Quick Actions."""
399
- # Find and remove all TonieToolbox Quick Actions
400
- for item in os.listdir(self.services_dir):
401
- if item.startswith("TonieToolbox - ") and item.endswith(".workflow"):
402
- action_path = os.path.join(self.services_dir, item)
403
- try:
404
- subprocess.run(["rm", "-rf", action_path], check=True)
405
- print(f"Removed: {item}")
406
- except subprocess.CalledProcessError:
407
- print(f"Failed to remove: {item}")
408
-
409
- # Refresh the Services menu
410
- subprocess.run(["killall", "-HUP", "Finder"], check=False)
546
+ """
547
+ Uninstall all TonieToolbox Quick Actions.
411
548
 
412
- print("TonieToolbox Quick Actions uninstalled successfully.")
413
-
549
+ Returns:
550
+ bool: True if all actions were uninstalled successfully, False otherwise.
551
+ """
552
+ try:
553
+ any_failures = False
554
+ for item in os.listdir(self.services_dir):
555
+ if item.startswith("TonieToolbox - ") and item.endswith(".workflow"):
556
+ action_path = os.path.join(self.services_dir, item)
557
+ try:
558
+ subprocess.run(["rm", "-rf", action_path], check=True)
559
+ print(f"Removed: {item}")
560
+ except subprocess.CalledProcessError as e:
561
+ print(f"Failed to remove: {item}")
562
+ logger.error(f"Error removing {item}: {e}")
563
+ any_failures = True
564
+ subprocess.run(["killall", "-HUP", "Finder"], check=False)
565
+
566
+ print("TonieToolbox Quick Actions uninstalled successfully.")
567
+
568
+ return not any_failures
569
+ except Exception as e:
570
+ logger.error(f"Failed to uninstall Quick Actions: {e}")
571
+ return False
572
+
414
573
  @classmethod
415
574
  def install(cls):
416
575
  """
417
576
  Generate Quick Actions and install them.
577
+
578
+ Returns:
579
+ bool: True if installation was successful, False otherwise.
418
580
  """
419
581
  instance = cls()
420
- instance.install_quick_actions()
582
+ if instance.install_quick_actions():
583
+ logger.info("macOS integration installed successfully.")
584
+ return True
585
+ else:
586
+ logger.error("macOS integration installation failed.")
587
+ return False
421
588
 
422
589
  @classmethod
423
590
  def uninstall(cls):
424
591
  """
425
592
  Uninstall all TonieToolbox Quick Actions.
593
+
594
+ Returns:
595
+ bool: True if uninstallation was successful, False otherwise.
426
596
  """
427
597
  instance = cls()
428
- instance.uninstall_quick_actions()
598
+ if instance.uninstall_quick_actions():
599
+ logger.info("macOS integration uninstalled successfully.")
600
+ return True
601
+ else:
602
+ logger.error("macOS integration uninstallation failed.")
603
+ return False
@@ -2,7 +2,7 @@
2
2
  import os
3
3
  import sys
4
4
  import json
5
- from .constants import SUPPORTED_EXTENSIONS
5
+ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE
6
6
  from .logger import get_logger
7
7
 
8
8
  logger = get_logger('integration_windows')
@@ -24,8 +24,7 @@ class WindowsClassicContextMenuIntegration:
24
24
  self.separator_above = '"CommandFlags"=dword:00000020'
25
25
  self.error_handling = r' && if %ERRORLEVEL% neq 0 (echo Error: Command failed with error code %ERRORLEVEL% && pause && exit /b %ERRORLEVEL%) else (echo Command completed successfully && ping -n 2 127.0.0.1 > nul)'
26
26
  self.show_info_error_handling = r' && if %ERRORLEVEL% neq 0 (echo Error: Command failed with error code %ERRORLEVEL% && pause && exit /b %ERRORLEVEL%) else (echo. && echo Press any key to close this window... && pause > nul)'
27
- self.config = self._load_config()
28
- # Ensure these attributes always exist
27
+ self.config = self._apply_config_template()
29
28
  self.upload_url = ''
30
29
  self.log_level = self.config.get('log_level', 'SILENT')
31
30
  self.log_to_file = self.config.get('log_to_file', False)
@@ -127,6 +126,18 @@ class WindowsClassicContextMenuIntegration:
127
126
  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)
128
127
  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)
129
128
 
129
+ def _apply_config_template(self):
130
+ """Apply the default configuration template if config.json is missing or invalid."""
131
+ config_path = os.path.join(self.output_dir, 'config.json')
132
+ if not os.path.exists(config_path):
133
+ with open(config_path, 'w') as f:
134
+ json.dump(CONFIG_TEMPLATE, f, indent=4)
135
+ logger.debug(f"Default configuration created at {config_path}")
136
+ return CONFIG_TEMPLATE
137
+ else:
138
+ logger.debug(f"Configuration file found at {config_path}")
139
+ return self._load_config()
140
+
130
141
  def _load_config(self):
131
142
  """Load configuration settings from config.json"""
132
143
  config_path = os.path.join(self.output_dir, 'config.json')
@@ -360,11 +371,14 @@ class WindowsClassicContextMenuIntegration:
360
371
  f.write('\n'.join(unreg_lines))
361
372
 
362
373
  return reg_path
363
-
374
+
364
375
  def install_registry_files(self, uninstall=False):
365
376
  """
366
377
  Import the generated .reg file into the Windows registry with UAC elevation.
367
378
  If uninstall is True, imports the uninstaller .reg file.
379
+
380
+ Returns:
381
+ bool: True if registry import was successful, False otherwise.
368
382
  """
369
383
  import subprocess
370
384
  reg_file = os.path.join(
@@ -372,18 +386,27 @@ class WindowsClassicContextMenuIntegration:
372
386
  'remove_tonietoolbox_context.reg' if uninstall else 'tonietoolbox_context.reg'
373
387
  )
374
388
  if not os.path.exists(reg_file):
375
- raise FileNotFoundError(f"Registry file not found: {reg_file}")
389
+ logger.error(f"Registry file not found: {reg_file}")
390
+ return False
376
391
 
377
- # Use PowerShell to run reg.exe import as administrator (fix argument passing)
378
392
  ps_command = (
379
- f"Start-Process reg.exe -ArgumentList @('import', '{reg_file}') -Verb RunAs"
393
+ f"Start-Process reg.exe -ArgumentList @('import', '{reg_file}') -Verb RunAs -Wait -PassThru"
380
394
  )
381
395
  try:
382
- subprocess.run(["powershell.exe", "-Command", ps_command], check=True)
383
- print(f"{'Uninstallation' if uninstall else 'Installation'} registry import completed.")
384
- except subprocess.CalledProcessError as e:
385
- print(f"Failed to import registry file: {e}")
386
- raise
396
+ result = subprocess.run(["powershell.exe", "-Command", ps_command], check=False,
397
+ capture_output=True, text=True)
398
+
399
+ if result.returncode == 0:
400
+ logger.info(f"{'Uninstallation' if uninstall else 'Installation'} registry import completed.")
401
+ return True
402
+ else:
403
+ logger.error(f"Registry import command failed with return code {result.returncode}")
404
+ logger.error(f"STDERR: {result.stderr}")
405
+ return False
406
+
407
+ except subprocess.SubprocessError as e:
408
+ logger.error(f"Failed to import registry file: {e}")
409
+ return False
387
410
 
388
411
  @classmethod
389
412
  def install(cls):
@@ -392,7 +415,12 @@ class WindowsClassicContextMenuIntegration:
392
415
  """
393
416
  instance = cls()
394
417
  instance.generate_registry_files()
395
- instance.install_registry_files(uninstall=False)
418
+ if instance.install_registry_files(uninstall=False):
419
+ logger.info("Integration installed successfully.")
420
+ return True
421
+ else:
422
+ logger.error("Integration installation failed.")
423
+ return False
396
424
 
397
425
  @classmethod
398
426
  def uninstall(cls):
@@ -401,4 +429,9 @@ class WindowsClassicContextMenuIntegration:
401
429
  """
402
430
  instance = cls()
403
431
  instance.generate_registry_files()
404
- instance.install_registry_files(uninstall=True)
432
+ if instance.install_registry_files(uninstall=True):
433
+ logger.info("Integration uninstalled successfully.")
434
+ return True
435
+ else:
436
+ logger.error("Integration uninstallation failed.")
437
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: TonieToolbox
3
- Version: 0.6.0a1
3
+ Version: 0.6.0a3
4
4
  Summary: Create files for the Tonie box and interact with TeddyCloud servers
5
5
  Home-page: https://github.com/Quentendo64/TonieToolbox
6
6
  Author: Quentendo64
@@ -225,7 +225,7 @@ docker run --rm -v "$(pwd)/input:/tonietoolbox/input" -v "$(pwd)/output:/tonieto
225
225
  docker run --rm -v "%cd%\input:/tonietoolbox/input" -v "%cd%\output:/tonietoolbox/output" quentendo64/tonietoolbox input/my-audio-file.mp3
226
226
  ```
227
227
 
228
- **Or using docker-compose**
228
+ **Or using docker-compose:**
229
229
 
230
230
  ```shell
231
231
  docker-compose run --rm tonietoolbox input/my-audio-file.mp3
@@ -1,15 +1,14 @@
1
- TonieToolbox/__init__.py,sha256=H9C8hvC7PUd27ggMybMlCYcOkSCis5TDSDcwhNFMNxI,98
2
- TonieToolbox/__main__.py,sha256=dKZ4d6LvLyNZRLKoGvFgi6_qdsv3lljNjC0TocDIt_w,33724
1
+ TonieToolbox/__init__.py,sha256=03DkelZsljyJIZJPxhaIrnGaLVecnU_C3sxGmHN3a_0,98
2
+ TonieToolbox/__main__.py,sha256=pD-goy-ke-M3Z8zkbUgW1IF94UQPGQSHql15yeBBDzk,34040
3
3
  TonieToolbox/artwork.py,sha256=fOlYx1cffVq-2u1e_qiW5bd3LpPCGMmIfZAxIwC05tA,4451
4
4
  TonieToolbox/audio_conversion.py,sha256=0GpC6mSRYikIjf_A1w26LAnYtCP2gpHLEKozOISapnM,17190
5
- TonieToolbox/config.py,sha256=5GEofOU10aPT2E5nvYOeClNK73ODVeh2zhykMbBmgnI,22
6
- TonieToolbox/constants.py,sha256=c8wwWGgjgsj1V4XiYizhY6bXLwa80AchnICvWSDkimg,6789
7
- TonieToolbox/dependency_manager.py,sha256=EvVUO4T1CrhUXlrVk9HBgCRDER3b1BRNdgkZLSpCTho,27921
5
+ TonieToolbox/constants.py,sha256=4L-lOX0_UBt3Xo5_Y2tVKhlHCRysjTIbC-cdlE7lNl0,8104
6
+ TonieToolbox/dependency_manager.py,sha256=GB1RlXNkRHsEd4m7x45-uqly6kVbnyr9wlOmxHzAzgc,28392
8
7
  TonieToolbox/filename_generator.py,sha256=ATCG4w8uN1vyAqvmdhOtpJLlb9QFKCnYIdBViYqpHjw,3464
9
- TonieToolbox/integration.py,sha256=R0amnTvBj6IlILk8SgNlrSO-0SRIpzqBjAXh7CtYiyk,948
10
- TonieToolbox/integration_macos.py,sha256=feaErW81WvkHvO-CP7vGOLIrFVoEdzWH1BkkiNuxyyU,17970
8
+ TonieToolbox/integration.py,sha256=NEQpKQSwLa6el7DoIqWbscBUGmRrYls_UiSmmvpSpxw,2785
9
+ TonieToolbox/integration_macos.py,sha256=bWAQ-vWrQK-KluL3NYdZexEUBwnkL_-tasFgkHL3yK4,26907
11
10
  TonieToolbox/integration_ubuntu.py,sha256=MU6W0xRCdoHBxrIiOIHePqYTF5Wvn4JxBnDQUPf6fgg,33
12
- TonieToolbox/integration_windows.py,sha256=LMDkOy2wo5iGTp-kVzRhz_0MzNu42IWNBf-VdvAQQ8A,22280
11
+ TonieToolbox/integration_windows.py,sha256=SSVg-9bf8wDQMuJgg-j_6cUczcx7H1-C1mbEIlNJlzs,23691
13
12
  TonieToolbox/logger.py,sha256=Q_cXbCWfzNmt5q6fvVzeM8IugkD24CSZAVjuf16n6b4,3120
14
13
  TonieToolbox/media_tags.py,sha256=oDlLe0AyvmIdQlqPzH74AUCqwbZZ-49AQKAJdrW26XE,20830
15
14
  TonieToolbox/ogg_page.py,sha256=IHdP0er0TYjyLfON8zes11FdQtRab3QNxeK6sxnAX08,22340
@@ -23,9 +22,9 @@ TonieToolbox/tonie_header.proto,sha256=WaWfwO4VrwGtscK2ujfDRKtpeBpaVPoZhI8iMmR-C
23
22
  TonieToolbox/tonie_header_pb2.py,sha256=s5bp4ULTEekgq6T61z9fDkRavyPM-3eREs20f_Pxxe8,3665
24
23
  TonieToolbox/tonies_json.py,sha256=YGS2wtaDudxxSy7QuRLWaE5n4bf_AyoSvVLH1Vdh8SE,60754
25
24
  TonieToolbox/version_handler.py,sha256=MLpJ9mSEHkcSoyePnACpfINHTSB1q1_4iEgcT1tGqNU,10028
26
- tonietoolbox-0.6.0a1.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
27
- tonietoolbox-0.6.0a1.dist-info/METADATA,sha256=zCDKsxxfyJmQ_a1X_9kAjlJhTv8DErsxbM2udCXTl0E,26848
28
- tonietoolbox-0.6.0a1.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
29
- tonietoolbox-0.6.0a1.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
30
- tonietoolbox-0.6.0a1.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
31
- tonietoolbox-0.6.0a1.dist-info/RECORD,,
25
+ tonietoolbox-0.6.0a3.dist-info/licenses/LICENSE.md,sha256=rGoga9ZAgNco9fBapVFpWf6ri7HOBp1KRnt1uIruXMk,35190
26
+ tonietoolbox-0.6.0a3.dist-info/METADATA,sha256=N6HxrJmWGLbiTvwXInNLMHsptNPJagYkg3TACTODc0M,26849
27
+ tonietoolbox-0.6.0a3.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
28
+ tonietoolbox-0.6.0a3.dist-info/entry_points.txt,sha256=oqpeyBxel7aScg35Xr4gZKnf486S5KW9okqeBwyJxxc,60
29
+ tonietoolbox-0.6.0a3.dist-info/top_level.txt,sha256=Wkkm-2p7I3ENfS7ZbYtYUB2g-xwHrXVlERHfonsOPuE,13
30
+ tonietoolbox-0.6.0a3.dist-info/RECORD,,
TonieToolbox/config.py DELETED
@@ -1 +0,0 @@
1
- ## TODO: Add config.py