TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0a1__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,404 @@
1
+ # filepath: d:\Repository\TonieToolbox\TonieToolbox\integration_windows.py
2
+ import os
3
+ import sys
4
+ import json
5
+ from .constants import SUPPORTED_EXTENSIONS
6
+ from .logger import get_logger
7
+
8
+ logger = get_logger('integration_windows')
9
+
10
+ class WindowsClassicContextMenuIntegration:
11
+ """
12
+ Class to generate Windows registry entries for TonieToolbox "classic" context menu integration.
13
+ Adds a 'TonieToolbox' cascade menu for supported audio files, .taf files, and folders.
14
+ """
15
+ def __init__(self):
16
+ self.exe_path = os.path.join(sys.prefix, 'Scripts', 'tonietoolbox.exe')
17
+ self.exe_path_reg = self.exe_path.replace('\\', r'\\')
18
+ self.output_dir = os.path.join(os.path.expanduser('~'), '.tonietoolbox')
19
+ self.icon_path = os.path.join(self.output_dir, 'icon.ico').replace('\\', r'\\')
20
+ self.cascade_name = 'TonieToolbox'
21
+ self.entry_is_separator = '"CommandFlags"=dword:00000008'
22
+ self.show_uac = '"CommandFlags"=dword:00000010'
23
+ self.separator_below = '"CommandFlags"=dword:00000040'
24
+ self.separator_above = '"CommandFlags"=dword:00000020'
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
+ 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
29
+ self.upload_url = ''
30
+ self.log_level = self.config.get('log_level', 'SILENT')
31
+ self.log_to_file = self.config.get('log_to_file', False)
32
+ self.basic_authentication_cmd = ''
33
+ self.client_cert_cmd = ''
34
+ self.upload_enabled = self._setup_upload()
35
+
36
+ print(f"Upload enabled: {self.upload_enabled}")
37
+ print(f"Upload URL: {self.upload_url}")
38
+ print(f"Authentication: {'Basic Authentication' if self.basic_authentication else ('None' if self.none_authentication else ('Client Cert' if self.client_cert_authentication else 'Unknown'))}")
39
+
40
+ self._setup_commands()
41
+
42
+ 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, shell='cmd.exe', keep_open=False, log_to_file=False):
43
+ """Dynamically build command strings for registry entries."""
44
+ exe = self.exe_path_reg
45
+ cmd = f'{shell} /{"k" if keep_open else "c"} "echo Running TonieToolbox'
46
+ if use_info:
47
+ cmd += ' info'
48
+ elif is_split:
49
+ cmd += ' split'
50
+ elif use_compare:
51
+ cmd += ' compare'
52
+ elif is_recursive:
53
+ cmd += ' recursive folder convert'
54
+ elif is_folder:
55
+ cmd += ' folder convert'
56
+ elif use_upload and use_artwork and use_json:
57
+ cmd += ' convert, upload, artwork and JSON'
58
+ elif use_upload and use_artwork:
59
+ cmd += ' convert, upload and artwork'
60
+ elif use_upload:
61
+ cmd += ' convert and upload'
62
+ else:
63
+ cmd += ' convert'
64
+ cmd += ' command... && "'
65
+ cmd += f'{exe}" {base_args}'
66
+ if log_to_file:
67
+ cmd += ' --log-file'
68
+ if is_recursive:
69
+ cmd += ' --recursive'
70
+ if output_to_source:
71
+ cmd += ' --output-to-source'
72
+ if use_info:
73
+ cmd += ' --info'
74
+ if is_split:
75
+ cmd += ' --split'
76
+ if use_compare:
77
+ cmd += ' --compare "%1" "%2"'
78
+ else:
79
+ cmd += f' "{file_placeholder}"'
80
+ if use_upload:
81
+ cmd += f' --upload "{self.upload_url}"'
82
+ if self.basic_authentication_cmd:
83
+ cmd += f' {self.basic_authentication_cmd}'
84
+ elif self.client_cert_cmd:
85
+ cmd += f' {self.client_cert_cmd}'
86
+ if getattr(self, "ignore_ssl_verify", False):
87
+ cmd += ' --ignore-ssl-verify'
88
+ if use_artwork:
89
+ cmd += ' --include-artwork'
90
+ if use_json:
91
+ cmd += ' --create-custom-json'
92
+ if use_info or use_compare:
93
+ cmd += ' && echo. && pause && exit > nul"'
94
+ else:
95
+ cmd += f'{self.error_handling}"'
96
+ return cmd
97
+
98
+ def _get_log_level_arg(self):
99
+ """Return the correct log level argument for TonieToolbox CLI based on self.log_level."""
100
+ level = str(self.log_level).strip().upper()
101
+ if level == 'DEBUG':
102
+ return '--debug'
103
+ elif level == 'INFO':
104
+ return '--info'
105
+ return '--silent'
106
+
107
+ def _setup_commands(self):
108
+ """Set up all command strings for registry entries dynamically."""
109
+ log_level_arg = self._get_log_level_arg()
110
+ # Audio file commands
111
+ self.convert_cmd = self._build_cmd(f'{log_level_arg}', log_to_file=self.log_to_file)
112
+ self.upload_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, log_to_file=self.log_to_file)
113
+ self.upload_artwork_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
114
+ 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)
115
+
116
+ # .taf file commands
117
+ self.show_info_cmd = self._build_cmd(log_level_arg, use_info=True, keep_open=True, log_to_file=self.log_to_file)
118
+ self.extract_opus_cmd = self._build_cmd(log_level_arg, is_split=True, log_to_file=self.log_to_file)
119
+ self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
120
+ self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
121
+ 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)
122
+ self.compare_taf_cmd = self._build_cmd(log_level_arg, use_compare=True, keep_open=True, log_to_file=self.log_to_file)
123
+
124
+ # Folder commands
125
+ self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
126
+ 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)
127
+ 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
+ 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
+
130
+ def _load_config(self):
131
+ """Load configuration settings from config.json"""
132
+ config_path = os.path.join(self.output_dir, 'config.json')
133
+ if not os.path.exists(config_path):
134
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
135
+
136
+ with open(config_path, 'r') as f:
137
+ config = json.loads(f.read())
138
+
139
+ return config
140
+
141
+ def _setup_upload(self):
142
+ """Set up upload functionality based on config.json settings"""
143
+ # Always initialize authentication flags
144
+ self.basic_authentication = False
145
+ self.client_cert_authentication = False
146
+ self.none_authentication = False
147
+ config = self.config
148
+ try:
149
+ upload_config = config.get('upload', {})
150
+ self.upload_urls = upload_config.get('url', [])
151
+ self.ignore_ssl_verify = upload_config.get('ignore_ssl_verify', False)
152
+ self.username = upload_config.get('username', '')
153
+ self.password = upload_config.get('password', '')
154
+ self.basic_authentication_cmd = ''
155
+ self.client_cert_cmd = ''
156
+ if self.username and self.password:
157
+ self.basic_authentication_cmd = f'--username {self.username} --password {self.password}'
158
+ self.basic_authentication = True
159
+ self.client_cert_path = upload_config.get('client_cert_path', '')
160
+ self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
161
+ if self.client_cert_path and self.client_cert_key_path:
162
+ self.client_cert_cmd = f'--client-cert {self.client_cert_path} --client-cert-key {self.client_cert_key_path}'
163
+ self.client_cert_authentication = True
164
+ if self.client_cert_authentication and self.basic_authentication:
165
+ logger.warning("Both client certificate and basic authentication are set. Only one can be used.")
166
+ return False
167
+ self.upload_url = self.upload_urls[0] if self.upload_urls else ''
168
+ if not self.client_cert_authentication and not self.basic_authentication and self.upload_url:
169
+ self.none_authentication = True
170
+ return bool(self.upload_url)
171
+ except FileNotFoundError:
172
+ logger.debug("Configuration file not found. Skipping upload setup.")
173
+ return False
174
+ except json.JSONDecodeError:
175
+ logger.debug("Error decoding JSON in configuration file. Skipping upload setup.")
176
+ return False
177
+ except Exception as e:
178
+ logger.debug(f"Unexpected error while loading configuration: {e}")
179
+ return False
180
+
181
+ def _reg_escape(self, s):
182
+ """Escape a string for use in a .reg file (escape double quotes)."""
183
+ return s.replace('"', '\\"')
184
+
185
+ def _generate_audio_extensions_entries(self):
186
+ """Generate registry entries for supported audio file extensions"""
187
+ reg_lines = []
188
+ for ext in SUPPORTED_EXTENSIONS:
189
+ ext = ext.lower().lstrip('.')
190
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell]')
191
+ reg_lines.append('')
192
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}]')
193
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
194
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
195
+ reg_lines.append('"subcommands"=""')
196
+ reg_lines.append('')
197
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell]')
198
+ # Convert
199
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\a_Convert]')
200
+ reg_lines.append('@="Convert File to .taf"')
201
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\a_Convert\\command]')
202
+ reg_lines.append(f'@="{self._reg_escape(self.convert_cmd)}"')
203
+ reg_lines.append('')
204
+ if self.upload_enabled:
205
+ # Upload
206
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\b_Upload]')
207
+ reg_lines.append('@="Convert File to .taf and Upload"')
208
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\b_Upload\\command]')
209
+ reg_lines.append(f'@="{self._reg_escape(self.upload_cmd)}"')
210
+ reg_lines.append('')
211
+ # Upload + Artwork
212
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\c_UploadArtwork]')
213
+ reg_lines.append('@="Convert File to .taf and Upload + Artwork"')
214
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\c_UploadArtwork\\command]')
215
+ reg_lines.append(f'@="{self._reg_escape(self.upload_artwork_cmd)}"')
216
+ reg_lines.append('')
217
+ # Upload + Artwork + JSON
218
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\d_UploadArtworkJson]')
219
+ reg_lines.append('@="Convert File to .taf and Upload + Artwork + JSON"')
220
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}\\shell\\d_UploadArtworkJson\\command]')
221
+ reg_lines.append(f'@="{self._reg_escape(self.upload_artwork_json_cmd)}"')
222
+ reg_lines.append('')
223
+ return reg_lines
224
+
225
+ def _generate_taf_file_entries(self):
226
+ """Generate registry entries for .taf files"""
227
+ reg_lines = []
228
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell]')
229
+ reg_lines.append('')
230
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}]')
231
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
232
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
233
+ reg_lines.append('"subcommands"=""')
234
+ reg_lines.append('')
235
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell]')
236
+ # Show Info
237
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\a_ShowInfo]')
238
+ reg_lines.append('@="Show Info"')
239
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\a_ShowInfo\\command]')
240
+ reg_lines.append(f'@="{self._reg_escape(self.show_info_cmd)}"')
241
+ reg_lines.append('')
242
+ # Extract Opus Tracks
243
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\b_ExtractOpus]')
244
+ reg_lines.append('@="Extract Opus Tracks"')
245
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\b_ExtractOpus\\command]')
246
+ reg_lines.append(f'@="{self._reg_escape(self.extract_opus_cmd)}"')
247
+ reg_lines.append('')
248
+ if self.upload_enabled:
249
+ # Upload
250
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\c_Upload]')
251
+ reg_lines.append('@="Upload"')
252
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\c_Upload\\command]')
253
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_cmd)}"')
254
+ reg_lines.append('')
255
+ # Upload + Artwork
256
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\d_UploadArtwork]')
257
+ reg_lines.append('@="Upload + Artwork"')
258
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\d_UploadArtwork\\command]')
259
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_artwork_cmd)}"')
260
+ reg_lines.append('')
261
+ # Upload + Artwork + JSON
262
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\e_UploadArtworkJson]')
263
+ reg_lines.append('@="Upload + Artwork + JSON"')
264
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\e_UploadArtworkJson\\command]')
265
+ reg_lines.append(f'@="{self._reg_escape(self.upload_taf_artwork_json_cmd)}"')
266
+ reg_lines.append('')
267
+ # Compare TAF Files
268
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\f_CompareTaf]')
269
+ reg_lines.append('@="Compare with another .taf file"')
270
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}\\shell\\f_CompareTaf\\command]')
271
+ reg_lines.append(f'@="{self._reg_escape(self.compare_taf_cmd)}"')
272
+ reg_lines.append('')
273
+ return reg_lines
274
+
275
+ def _generate_folder_entries(self):
276
+ """Generate registry entries for folders"""
277
+ reg_lines = []
278
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell]')
279
+ reg_lines.append('')
280
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}]')
281
+ reg_lines.append('"MUIVerb"="TonieToolbox"')
282
+ reg_lines.append(f'"Icon"="{self.icon_path}"')
283
+ reg_lines.append('"subcommands"=""')
284
+ reg_lines.append('')
285
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell]')
286
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\a_ConvertFolder]')
287
+ reg_lines.append('@="Convert Folder to .taf (recursive)"')
288
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\a_ConvertFolder\\command]')
289
+ reg_lines.append(f'@="{self._reg_escape(self.convert_folder_cmd)}"')
290
+ reg_lines.append('')
291
+ if self.upload_enabled:
292
+ # Upload
293
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\b_UploadFolder]')
294
+ reg_lines.append('@="Convert Folder to .taf and Upload (recursive)"')
295
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\b_UploadFolder\\command]')
296
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_cmd)}"')
297
+ reg_lines.append('')
298
+ # Upload + Artwork
299
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\c_UploadFolderArtwork]')
300
+ reg_lines.append('@="Convert Folder to .taf and Upload + Artwork (recursive)"')
301
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\c_UploadFolderArtwork\\command]')
302
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_artwork_cmd)}"')
303
+ reg_lines.append('')
304
+ # Upload + Artwork + JSON
305
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\d_UploadFolderArtworkJson]')
306
+ reg_lines.append('@="Convert Folder to .taf and Upload + Artwork + JSON (recursive)"')
307
+ reg_lines.append(f'[HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}\\shell\\d_UploadFolderArtworkJson\\command]')
308
+ reg_lines.append(f'@="{self._reg_escape(self.upload_folder_artwork_json_cmd)}"')
309
+ reg_lines.append('')
310
+ return reg_lines
311
+
312
+ def _generate_uninstaller_entries(self):
313
+ """Generate registry entries for uninstaller"""
314
+ unreg_lines = [
315
+ 'Windows Registry Editor Version 5.00',
316
+ '',
317
+ ]
318
+
319
+ for ext in SUPPORTED_EXTENSIONS:
320
+ ext = ext.lower().lstrip('.')
321
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\SystemFileAssociations\\.{ext}\\shell\\{self.cascade_name}]')
322
+ unreg_lines.append('')
323
+
324
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\SystemFileAssociations\\.taf\\shell\\{self.cascade_name}]')
325
+ unreg_lines.append('')
326
+ unreg_lines.append(f'[-HKEY_CLASSES_ROOT\\Directory\\shell\\{self.cascade_name}]')
327
+
328
+ return unreg_lines
329
+
330
+ def generate_registry_files(self):
331
+ """
332
+ Generate Windows registry files for TonieToolbox context menu integration.
333
+ Returns the path to the installer registry file.
334
+ """
335
+ os.makedirs(self.output_dir, exist_ok=True)
336
+
337
+ reg_lines = [
338
+ 'Windows Registry Editor Version 5.00',
339
+ '',
340
+ ]
341
+
342
+ # Add entries for audio extensions
343
+ reg_lines.extend(self._generate_audio_extensions_entries())
344
+
345
+ # Add entries for .taf files
346
+ reg_lines.extend(self._generate_taf_file_entries())
347
+
348
+ # Add entries for folders
349
+ reg_lines.extend(self._generate_folder_entries())
350
+
351
+ # Write the installer .reg file
352
+ reg_path = os.path.join(self.output_dir, 'tonietoolbox_context.reg')
353
+ with open(reg_path, 'w', encoding='utf-8') as f:
354
+ f.write('\n'.join(reg_lines))
355
+
356
+ # Generate and write the uninstaller .reg file
357
+ unreg_lines = self._generate_uninstaller_entries()
358
+ unreg_path = os.path.join(self.output_dir, 'remove_tonietoolbox_context.reg')
359
+ with open(unreg_path, 'w', encoding='utf-8') as f:
360
+ f.write('\n'.join(unreg_lines))
361
+
362
+ return reg_path
363
+
364
+ def install_registry_files(self, uninstall=False):
365
+ """
366
+ Import the generated .reg file into the Windows registry with UAC elevation.
367
+ If uninstall is True, imports the uninstaller .reg file.
368
+ """
369
+ import subprocess
370
+ reg_file = os.path.join(
371
+ self.output_dir,
372
+ 'remove_tonietoolbox_context.reg' if uninstall else 'tonietoolbox_context.reg'
373
+ )
374
+ if not os.path.exists(reg_file):
375
+ raise FileNotFoundError(f"Registry file not found: {reg_file}")
376
+
377
+ # Use PowerShell to run reg.exe import as administrator (fix argument passing)
378
+ ps_command = (
379
+ f"Start-Process reg.exe -ArgumentList @('import', '{reg_file}') -Verb RunAs"
380
+ )
381
+ 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
387
+
388
+ @classmethod
389
+ def install(cls):
390
+ """
391
+ Generate registry files and install them with UAC elevation.
392
+ """
393
+ instance = cls()
394
+ instance.generate_registry_files()
395
+ instance.install_registry_files(uninstall=False)
396
+
397
+ @classmethod
398
+ def uninstall(cls):
399
+ """
400
+ Generate registry files and uninstall them with UAC elevation.
401
+ """
402
+ instance = cls()
403
+ instance.generate_registry_files()
404
+ instance.install_registry_files(uninstall=True)
TonieToolbox/logger.py CHANGED
@@ -13,7 +13,7 @@ TRACE = 5 # Custom level for ultra-verbose debugging
13
13
  logging.addLevelName(TRACE, 'TRACE')
14
14
 
15
15
  # Create a method for the TRACE level
16
- def trace(self, message, *args, **kwargs):
16
+ def trace(self: logging.Logger, message: str, *args, **kwargs) -> None:
17
17
  """Log a message with TRACE level (more detailed than DEBUG)"""
18
18
  if self.isEnabledFor(TRACE):
19
19
  self.log(TRACE, message, *args, **kwargs)
@@ -21,7 +21,7 @@ def trace(self, message, *args, **kwargs):
21
21
  # Add trace method to the Logger class
22
22
  logging.Logger.trace = trace
23
23
 
24
- def get_log_file_path():
24
+ def get_log_file_path() -> Path:
25
25
  """
26
26
  Get the path to the log file in the .tonietoolbox folder with timestamp.
27
27
 
@@ -29,7 +29,7 @@ def get_log_file_path():
29
29
  Path: Path to the log file
30
30
  """
31
31
  # Create .tonietoolbox folder in user's home directory if it doesn't exist
32
- log_dir = Path.home() / '.tonietoolbox'
32
+ log_dir = Path.home() / '.tonietoolbox' / 'logs'
33
33
  log_dir.mkdir(exist_ok=True)
34
34
 
35
35
  # Create timestamp string for the filename
@@ -40,14 +40,13 @@ def get_log_file_path():
40
40
 
41
41
  return log_file
42
42
 
43
- def setup_logging(level=logging.INFO, log_to_file=False):
43
+ def setup_logging(level: int = logging.INFO, log_to_file: bool = False) -> logging.Logger:
44
44
  """
45
45
  Set up logging configuration for the entire application.
46
46
 
47
47
  Args:
48
- level: Logging level (default: logging.INFO)
49
- log_to_file: Whether to log to a file (default: False)
50
-
48
+ level (int): Logging level (default: logging.INFO)
49
+ log_to_file (bool): Whether to log to a file (default: False)
51
50
  Returns:
52
51
  logging.Logger: Root logger instance
53
52
  """
@@ -88,13 +87,12 @@ def setup_logging(level=logging.INFO, log_to_file=False):
88
87
 
89
88
  return root_logger
90
89
 
91
- def get_logger(name):
90
+ def get_logger(name: str) -> logging.Logger:
92
91
  """
93
92
  Get a logger with the specified name.
94
93
 
95
94
  Args:
96
- name: Logger name, typically the module name
97
-
95
+ name (str): Logger name, typically the module name
98
96
  Returns:
99
97
  logging.Logger: Logger instance
100
98
  """
@@ -13,8 +13,8 @@ import base64
13
13
  from mutagen.flac import Picture
14
14
  from .logger import get_logger
15
15
  from .dependency_manager import is_mutagen_available, ensure_mutagen
16
+ from .constants import ARTWORK_NAMES, ARTWORK_EXTENSIONS, TAG_VALUE_REPLACEMENTS, TAG_MAPPINGS
16
17
 
17
- # Global variables to track dependency state and store module references
18
18
  MUTAGEN_AVAILABLE = False
19
19
  mutagen = None
20
20
  ID3 = None
@@ -53,84 +53,11 @@ def _import_mutagen():
53
53
  MUTAGEN_AVAILABLE = False
54
54
  return False
55
55
 
56
- # Try to import mutagen if it's available
57
56
  if is_mutagen_available():
58
57
  _import_mutagen()
59
58
 
60
59
  logger = get_logger('media_tags')
61
60
 
62
- # Define tag mapping for different formats to standardized names
63
- # This helps normalize tags across different audio formats
64
- TAG_MAPPING = {
65
- # ID3 (MP3) tags
66
- 'TIT2': 'title',
67
- 'TALB': 'album',
68
- 'TPE1': 'artist',
69
- 'TPE2': 'albumartist',
70
- 'TCOM': 'composer',
71
- 'TRCK': 'tracknumber',
72
- 'TPOS': 'discnumber',
73
- 'TDRC': 'date',
74
- 'TCON': 'genre',
75
- 'TPUB': 'publisher',
76
- 'TCOP': 'copyright',
77
- 'COMM': 'comment',
78
-
79
- # Vorbis tags (FLAC, OGG)
80
- 'title': 'title',
81
- 'album': 'album',
82
- 'artist': 'artist',
83
- 'albumartist': 'albumartist',
84
- 'composer': 'composer',
85
- 'tracknumber': 'tracknumber',
86
- 'discnumber': 'discnumber',
87
- 'date': 'date',
88
- 'genre': 'genre',
89
- 'publisher': 'publisher',
90
- 'copyright': 'copyright',
91
- 'comment': 'comment',
92
-
93
- # MP4 (M4A, AAC) tags
94
- '©nam': 'title',
95
- '©alb': 'album',
96
- '©ART': 'artist',
97
- 'aART': 'albumartist',
98
- '©wrt': 'composer',
99
- 'trkn': 'tracknumber',
100
- 'disk': 'discnumber',
101
- '©day': 'date',
102
- '©gen': 'genre',
103
- '©pub': 'publisher',
104
- 'cprt': 'copyright',
105
- '©cmt': 'comment',
106
-
107
- # Additional tags some files might have
108
- 'album_artist': 'albumartist',
109
- 'track': 'tracknumber',
110
- 'track_number': 'tracknumber',
111
- 'disc': 'discnumber',
112
- 'disc_number': 'discnumber',
113
- 'year': 'date',
114
- 'albuminterpret': 'albumartist', # German tag name
115
- 'interpret': 'artist', # German tag name
116
- }
117
-
118
- # Define replacements for special tag values
119
- TAG_VALUE_REPLACEMENTS = {
120
- "Die drei ???": "Die drei Fragezeichen",
121
- "Die Drei ???": "Die drei Fragezeichen",
122
- "DIE DREI ???": "Die drei Fragezeichen",
123
- "Die drei !!!": "Die drei Ausrufezeichen",
124
- "Die Drei !!!": "Die drei Ausrufezeichen",
125
- "DIE DREI !!!": "Die drei Ausrufezeichen",
126
- "TKKG™": "TKKG",
127
- "Die drei ??? Kids": "Die drei Fragezeichen Kids",
128
- "Die Drei ??? Kids": "Die drei Fragezeichen Kids",
129
- "Bibi & Tina": "Bibi und Tina",
130
- "Benjamin Blümchen™": "Benjamin Blümchen",
131
- "???": "Fragezeichen",
132
- "!!!": "Ausrufezeichen",
133
- }
134
61
 
135
62
  def normalize_tag_value(value: str) -> str:
136
63
  """
@@ -146,7 +73,6 @@ def normalize_tag_value(value: str) -> str:
146
73
  if not value:
147
74
  return value
148
75
 
149
- # Check for direct replacements first
150
76
  if value in TAG_VALUE_REPLACEMENTS:
151
77
  logger.debug("Direct tag replacement: '%s' -> '%s'", value, TAG_VALUE_REPLACEMENTS[value])
152
78
  return TAG_VALUE_REPLACEMENTS[value]
@@ -158,10 +84,7 @@ def normalize_tag_value(value: str) -> str:
158
84
  original = result
159
85
  result = result.replace(pattern, replacement)
160
86
  logger.debug("Partial tag replacement: '%s' -> '%s'", original, result)
161
-
162
- # Special case for "Die drei ???" type patterns that might have been missed
163
- result = result.replace("???", "Fragezeichen")
164
-
87
+
165
88
  return result
166
89
 
167
90
  def is_available() -> bool:
@@ -213,58 +136,58 @@ def get_file_tags(file_path: str) -> Dict[str, Any]:
213
136
  id3 = audio if isinstance(audio, ID3) else audio.ID3
214
137
  for tag_key, tag_value in id3.items():
215
138
  tag_name = tag_key.split(':')[0] # Handle ID3 tags with colons
216
- if tag_name in TAG_MAPPING:
139
+ if tag_name in TAG_MAPPINGS:
217
140
  tag_value_str = str(tag_value)
218
- tags[TAG_MAPPING[tag_name]] = normalize_tag_value(tag_value_str)
141
+ tags[TAG_MAPPINGS[tag_name]] = normalize_tag_value(tag_value_str)
219
142
  except (AttributeError, TypeError) as e:
220
143
  logger.debug("Error accessing ID3 tags: %s", e)
221
144
  # Try alternative approach for ID3 tags
222
145
  try:
223
146
  if hasattr(audio, 'tags') and audio.tags:
224
147
  for tag_key in audio.tags.keys():
225
- if tag_key in TAG_MAPPING:
148
+ if tag_key in TAG_MAPPINGS:
226
149
  tag_value = audio.tags[tag_key]
227
150
  if hasattr(tag_value, 'text'):
228
151
  tag_value_str = str(tag_value.text[0]) if tag_value.text else ''
229
152
  else:
230
153
  tag_value_str = str(tag_value)
231
- tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
154
+ tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
232
155
  except Exception as e:
233
156
  logger.debug("Alternative ID3 tag reading failed: %s", e)
234
157
  elif isinstance(audio, (FLAC, OggOpus, OggVorbis)):
235
158
  # FLAC and OGG files
236
159
  for tag_key, tag_values in audio.items():
237
160
  tag_key_lower = tag_key.lower()
238
- if tag_key_lower in TAG_MAPPING:
161
+ if tag_key_lower in TAG_MAPPINGS:
239
162
  # Some tags might have multiple values, we'll take the first one
240
163
  tag_value = tag_values[0] if tag_values else ''
241
- tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value)
164
+ tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value)
242
165
  elif isinstance(audio, MP4):
243
166
  # MP4 files
244
167
  for tag_key, tag_value in audio.items():
245
- if tag_key in TAG_MAPPING:
168
+ if tag_key in TAG_MAPPINGS:
246
169
  if isinstance(tag_value, list):
247
170
  if tag_key in ('trkn', 'disk'):
248
171
  # Handle track and disc number tuples
249
172
  if tag_value and isinstance(tag_value[0], tuple) and len(tag_value[0]) >= 1:
250
- tags[TAG_MAPPING[tag_key]] = str(tag_value[0][0])
173
+ tags[TAG_MAPPINGS[tag_key]] = str(tag_value[0][0])
251
174
  else:
252
175
  tag_value_str = str(tag_value[0]) if tag_value else ''
253
- tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
176
+ tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
254
177
  else:
255
178
  tag_value_str = str(tag_value)
256
- tags[TAG_MAPPING[tag_key]] = normalize_tag_value(tag_value_str)
179
+ tags[TAG_MAPPINGS[tag_key]] = normalize_tag_value(tag_value_str)
257
180
  else:
258
181
  # Generic audio file - try to read any available tags
259
182
  for tag_key, tag_value in audio.items():
260
183
  tag_key_lower = tag_key.lower()
261
- if tag_key_lower in TAG_MAPPING:
184
+ if tag_key_lower in TAG_MAPPINGS:
262
185
  if isinstance(tag_value, list):
263
186
  tag_value_str = str(tag_value[0]) if tag_value else ''
264
- tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value_str)
187
+ tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value_str)
265
188
  else:
266
189
  tag_value_str = str(tag_value)
267
- tags[TAG_MAPPING[tag_key_lower]] = normalize_tag_value(tag_value_str)
190
+ tags[TAG_MAPPINGS[tag_key_lower]] = normalize_tag_value(tag_value_str)
268
191
 
269
192
  logger.debug("Successfully read %d tags from file", len(tags))
270
193
  logger.debug("Tags: %s", str(tags))
@@ -615,13 +538,10 @@ def find_cover_image(source_dir):
615
538
  return None
616
539
 
617
540
  # Common cover image file names
618
- cover_names = [
619
- 'cover', 'folder', 'album', 'front', 'artwork', 'image',
620
- 'albumart', 'albumartwork', 'booklet'
621
- ]
541
+ cover_names = ARTWORK_NAMES
622
542
 
623
543
  # Common image extensions
624
- image_extensions = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
544
+ image_extensions = ARTWORK_EXTENSIONS
625
545
 
626
546
  # Try different variations
627
547
  for name in cover_names: