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.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +85 -84
- TonieToolbox/artwork.py +12 -7
- TonieToolbox/audio_conversion.py +31 -28
- TonieToolbox/config.py +1 -0
- TonieToolbox/constants.py +93 -9
- TonieToolbox/filename_generator.py +6 -8
- TonieToolbox/integration.py +20 -0
- TonieToolbox/integration_macos.py +428 -0
- TonieToolbox/integration_ubuntu.py +1 -0
- TonieToolbox/integration_windows.py +404 -0
- TonieToolbox/logger.py +8 -10
- TonieToolbox/media_tags.py +17 -97
- TonieToolbox/ogg_page.py +39 -39
- TonieToolbox/opus_packet.py +13 -13
- TonieToolbox/recursive_processor.py +22 -22
- TonieToolbox/tags.py +3 -4
- TonieToolbox/teddycloud.py +50 -50
- TonieToolbox/tonie_analysis.py +24 -23
- TonieToolbox/tonie_file.py +71 -44
- TonieToolbox/tonies_json.py +69 -66
- TonieToolbox/version_handler.py +12 -15
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a1.dist-info}/METADATA +2 -2
- tonietoolbox-0.6.0a1.dist-info/RECORD +31 -0
- tonietoolbox-0.5.1.dist-info/RECORD +0 -26
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a1.dist-info}/WHEEL +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a1.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a1.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.5.1.dist-info → tonietoolbox-0.6.0a1.dist-info}/top_level.txt +0 -0
@@ -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
|
"""
|
TonieToolbox/media_tags.py
CHANGED
@@ -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
|
139
|
+
if tag_name in TAG_MAPPINGS:
|
217
140
|
tag_value_str = str(tag_value)
|
218
|
-
tags[
|
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
|
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[
|
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
|
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[
|
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
|
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[
|
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[
|
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[
|
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
|
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[
|
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[
|
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 =
|
544
|
+
image_extensions = ARTWORK_EXTENSIONS
|
625
545
|
|
626
546
|
# Try different variations
|
627
547
|
for name in cover_names:
|