TonieToolbox 0.6.0rc3__py3-none-any.whl → 0.6.4__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,677 @@
1
+ #!/usr/bin/python3
2
+ """
3
+ Integration for KDE with service menus.
4
+ This module generates KDE service menu entries (.desktop files) to add a 'TonieToolbox' submenu.
5
+ """
6
+ import os
7
+ import sys
8
+ import json
9
+ from .constants import SUPPORTED_EXTENSIONS, CONFIG_TEMPLATE, ICON_BASE64
10
+ from .logger import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ class KDEServiceMenuIntegration:
15
+ """
16
+ Class to generate KDE service menu entries for TonieToolbox integration.
17
+ Creates .desktop files in ~/.local/share/kservices5/ServiceMenus/ for supported audio files, .taf files, and folders.
18
+ """
19
+ def __init__(self):
20
+ # Find tonietoolbox executable
21
+ self.exe_path = self._find_executable()
22
+ self.output_dir = os.path.join(os.path.expanduser('~'), '.tonietoolbox')
23
+ # Try KDE6 first, then KDE5
24
+ kde6_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', 'kio', 'servicemenus')
25
+ kde5_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', 'kservices5', 'ServiceMenus')
26
+
27
+ # Check KDE version and use appropriate directory
28
+ kde_version = os.environ.get('KDE_SESSION_VERSION', '5')
29
+ if kde_version == '6':
30
+ self.service_menu_dir = kde6_dir
31
+ else:
32
+ self.service_menu_dir = kde5_dir
33
+
34
+ # Application directory for .desktop application files
35
+ self.application_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', 'applications')
36
+ self.icon_path = os.path.join(self.output_dir, 'icon.png')
37
+
38
+ # Load or create configuration
39
+ self.config = self._apply_config_template()
40
+ self.upload_url = ''
41
+ self.log_level = self.config.get('log_level', 'SILENT')
42
+ self.log_to_file = self.config.get('log_to_file', False)
43
+ self.basic_authentication_cmd = ''
44
+ self.client_cert_cmd = ''
45
+ self.upload_enabled = self._setup_upload()
46
+
47
+ print(f"Upload enabled: {self.upload_enabled}")
48
+ print(f"Upload URL: {self.upload_url}")
49
+ 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'))}")
50
+
51
+ self._setup_commands()
52
+
53
+ def _find_executable(self):
54
+ """Find the tonietoolbox executable."""
55
+ # Check common locations
56
+ possible_paths = [
57
+ os.path.join(sys.prefix, 'bin', 'tonietoolbox'),
58
+ '/usr/local/bin/tonietoolbox',
59
+ '/usr/bin/tonietoolbox',
60
+ os.path.expanduser('~/.local/bin/tonietoolbox')
61
+ ]
62
+
63
+ for path in possible_paths:
64
+ if os.path.isfile(path) and os.access(path, os.X_OK):
65
+ return path
66
+
67
+ # Try which command
68
+ import subprocess
69
+ try:
70
+ result = subprocess.run(['which', 'tonietoolbox'], capture_output=True, text=True)
71
+ if result.returncode == 0:
72
+ return result.stdout.strip()
73
+ except:
74
+ pass
75
+
76
+ # Fallback to just 'tonietoolbox' and hope it's in PATH
77
+ return 'tonietoolbox'
78
+
79
+ def _build_cmd(self, base_args, use_upload=False, use_artwork=False, use_json=False, use_compare=False, use_info=False, use_play=False, is_recursive=False, is_split=False, is_folder=False, keep_open=False, log_to_file=False):
80
+ """Dynamically build command strings for service menu entries."""
81
+ # Build the tonietoolbox command
82
+ tonietoolbox_cmd = f'{self.exe_path} {base_args}'
83
+
84
+ if log_to_file:
85
+ tonietoolbox_cmd += ' --log-file'
86
+ if is_recursive:
87
+ tonietoolbox_cmd += ' --recursive'
88
+ tonietoolbox_cmd += ' --output-to-source'
89
+ if use_info:
90
+ tonietoolbox_cmd += ' --info'
91
+ if use_play:
92
+ tonietoolbox_cmd += ' --play'
93
+ if is_split:
94
+ tonietoolbox_cmd += ' --split'
95
+ if use_compare:
96
+ tonietoolbox_cmd += ' --compare'
97
+ if use_upload:
98
+ tonietoolbox_cmd += f' --upload "{self.upload_url}"'
99
+ if self.basic_authentication_cmd:
100
+ tonietoolbox_cmd += f' {self.basic_authentication_cmd}'
101
+ elif self.client_cert_cmd:
102
+ tonietoolbox_cmd += f' {self.client_cert_cmd}'
103
+ if getattr(self, "ignore_ssl_verify", False):
104
+ tonietoolbox_cmd += ' --ignore-ssl-verify'
105
+ if use_artwork:
106
+ tonietoolbox_cmd += ' --include-artwork'
107
+ if use_json:
108
+ tonietoolbox_cmd += ' --create-custom-json'
109
+
110
+ return tonietoolbox_cmd
111
+
112
+ def _get_log_level_arg(self):
113
+ """Return the correct log level argument for TonieToolbox CLI based on self.log_level."""
114
+ level = str(self.log_level).strip().upper()
115
+ if level == 'DEBUG':
116
+ return '--debug'
117
+ elif level == 'INFO':
118
+ return '--info'
119
+ return '--silent'
120
+
121
+ def _setup_commands(self):
122
+ """Set up all command strings for service menu entries dynamically."""
123
+ log_level_arg = self._get_log_level_arg()
124
+
125
+ # Audio file commands
126
+ self.convert_cmd = self._build_cmd(f'{log_level_arg}', log_to_file=self.log_to_file)
127
+ self.upload_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, log_to_file=self.log_to_file)
128
+ self.upload_artwork_cmd = self._build_cmd(f'{log_level_arg}', use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
129
+ 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)
130
+
131
+ # .taf file commands
132
+ info_log_level = '--info' if self.log_level.upper() == 'SILENT' else log_level_arg
133
+ self.show_info_cmd = self._build_cmd(info_log_level, use_info=True, keep_open=True, log_to_file=self.log_to_file)
134
+ self.extract_opus_cmd = self._build_cmd(log_level_arg, is_split=True, log_to_file=self.log_to_file)
135
+ self.play_cmd = self._build_cmd(log_level_arg,use_play=True, keep_open=True, log_to_file=self.log_to_file)
136
+ self.upload_taf_cmd = self._build_cmd(log_level_arg, use_upload=True, log_to_file=self.log_to_file)
137
+ self.upload_taf_artwork_cmd = self._build_cmd(log_level_arg, use_upload=True, use_artwork=True, log_to_file=self.log_to_file)
138
+ 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)
139
+
140
+ # Folder commands
141
+ self.convert_folder_cmd = self._build_cmd(f'{log_level_arg}', is_recursive=True, is_folder=True, log_to_file=self.log_to_file)
142
+ 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)
143
+ 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)
144
+ 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)
145
+
146
+ def _apply_config_template(self):
147
+ """Apply the default configuration template if config.json is missing or invalid. Extracts the icon from base64 if not present."""
148
+ config_path = os.path.join(self.output_dir, 'config.json')
149
+ icon_path = os.path.join(self.output_dir, 'icon.png')
150
+ os.makedirs(self.output_dir, exist_ok=True)
151
+ # Extract icon to PNG for KDE (KDE prefers PNG over ICO)
152
+ if not os.path.exists(icon_path):
153
+ self._base64_to_png(ICON_BASE64, icon_path)
154
+ if not os.path.exists(self.icon_path):
155
+ self.icon_path = 'audio-x-generic'
156
+
157
+ if not os.path.exists(config_path):
158
+ with open(config_path, 'w') as f:
159
+ json.dump(CONFIG_TEMPLATE, f, indent=4)
160
+ logger.debug(f"Default configuration created at {config_path}")
161
+ return CONFIG_TEMPLATE
162
+ else:
163
+ logger.debug(f"Configuration file found at {config_path}")
164
+ return self._load_config()
165
+
166
+ def _base64_to_png(self, base64_data, output_path):
167
+ """Convert base64 ICO data to PNG format for KDE."""
168
+ try:
169
+ import base64
170
+ from PIL import Image
171
+ import io
172
+ ico_data = base64.b64decode(base64_data)
173
+ with Image.open(io.BytesIO(ico_data)) as img:
174
+ img.save(output_path, 'PNG')
175
+
176
+ except ImportError:
177
+ import base64
178
+ ico_data = base64.b64decode(base64_data)
179
+ ico_path = output_path.replace('.png', '.ico')
180
+ self._base64_to_ico(base64_data, ico_path)
181
+ self.icon_path = ico_path
182
+ except Exception as e:
183
+ logger.warning(f"Failed to convert icon: {e}")
184
+ self.icon_path = 'applications-multimedia'
185
+
186
+ def _base64_to_ico(self, base64_string, output_path):
187
+ """
188
+ Convert a base64 string back to an ICO file
189
+ """
190
+ import base64
191
+ ico_bytes = base64.b64decode(base64_string)
192
+
193
+ # Create directory if it doesn't exist
194
+ output_dir = os.path.dirname(output_path)
195
+ if output_dir and not os.path.exists(output_dir):
196
+ os.makedirs(output_dir)
197
+
198
+ with open(output_path, "wb") as ico_file:
199
+ ico_file.write(ico_bytes)
200
+
201
+ return output_path
202
+
203
+ def _load_config(self):
204
+ """Load configuration settings from config.json"""
205
+ config_path = os.path.join(self.output_dir, 'config.json')
206
+ if not os.path.exists(config_path):
207
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
208
+
209
+ with open(config_path, 'r') as f:
210
+ content = f.read().strip()
211
+ if not content:
212
+ # Empty file, return default config
213
+ logger.debug("Config file is empty, using default template")
214
+ return CONFIG_TEMPLATE
215
+ config = json.loads(content)
216
+
217
+ return config
218
+
219
+ def _setup_upload(self):
220
+ """Set up upload functionality based on config.json settings"""
221
+ self.basic_authentication = False
222
+ self.client_cert_authentication = False
223
+ self.none_authentication = False
224
+ config = self.config
225
+ try:
226
+ upload_config = config.get('upload', {})
227
+ self.upload_urls = upload_config.get('url', [])
228
+ self.ignore_ssl_verify = upload_config.get('ignore_ssl_verify', False)
229
+ self.username = upload_config.get('username', '')
230
+ self.password = upload_config.get('password', '')
231
+ self.basic_authentication_cmd = ''
232
+ self.client_cert_cmd = ''
233
+ if self.username and self.password:
234
+ self.basic_authentication_cmd = f'--username {self.username} --password {self.password}'
235
+ self.basic_authentication = True
236
+ self.client_cert_path = upload_config.get('client_cert_path', '')
237
+ self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
238
+ if self.client_cert_path and self.client_cert_key_path:
239
+ cert_path = os.path.expanduser(self.client_cert_path)
240
+ key_path = os.path.expanduser(self.client_cert_key_path)
241
+ self.client_cert_cmd = f'--client-cert "{cert_path}" --client-key "{key_path}"'
242
+ self.client_cert_authentication = True
243
+ if self.client_cert_authentication and self.basic_authentication:
244
+ logger.warning("Both client certificate and basic authentication are set. Only one can be used.")
245
+ return False
246
+ self.upload_url = self.upload_urls[0] if self.upload_urls else ''
247
+ if not self.client_cert_authentication and not self.basic_authentication and self.upload_url:
248
+ self.none_authentication = True
249
+ return bool(self.upload_url)
250
+ except FileNotFoundError:
251
+ logger.debug("Configuration file not found. Skipping upload setup.")
252
+ return False
253
+ except json.JSONDecodeError:
254
+ logger.debug("Error decoding JSON in configuration file. Skipping upload setup.")
255
+ return False
256
+ except Exception as e:
257
+ logger.debug(f"Unexpected error while loading configuration: {e}")
258
+ return False
259
+
260
+ def _generate_audio_extensions_entries(self):
261
+ """Generate KDE service menu entries for supported audio file extensions"""
262
+ mime_types = set()
263
+ for ext in SUPPORTED_EXTENSIONS:
264
+ ext = ext.lower().lstrip('.')
265
+ # Map common extensions to MIME types
266
+ mime_map = {
267
+ 'mp3': 'audio/mpeg',
268
+ 'wav': 'audio/wav',
269
+ 'flac': 'audio/flac',
270
+ 'ogg': 'audio/ogg',
271
+ 'opus': 'audio/opus',
272
+ 'aac': 'audio/aac',
273
+ 'm4a': 'audio/mp4',
274
+ 'wma': 'audio/x-ms-wma',
275
+ 'aiff': 'audio/x-aiff',
276
+ 'mp2': 'audio/mpeg',
277
+ 'mp4': 'audio/mp4',
278
+ 'webm': 'audio/webm',
279
+ 'mka': 'audio/x-matroska',
280
+ 'ape': 'audio/x-ape'
281
+ }
282
+ if ext in mime_map:
283
+ mime_types.add(mime_map[ext])
284
+ mime_types_list = sorted(list(mime_types))
285
+ mime_types_str = ';'.join(mime_types_list) + ';'
286
+
287
+ actions = ["convert"]
288
+ if self.upload_enabled:
289
+ actions.extend(["upload", "upload_artwork", "upload_artwork_json"])
290
+ actions_str = ';'.join(actions)
291
+
292
+ desktop_content = f"""[Desktop Entry]
293
+ Type=Service
294
+ ServiceTypes=KonqPopupMenu/Plugin
295
+ MimeType={mime_types_str}
296
+ Actions={actions_str}
297
+ X-KDE-Submenu=TonieToolbox
298
+ X-KDE-Submenu[de]=TonieToolbox
299
+ Icon={self.icon_path}
300
+
301
+ [Desktop Action convert]
302
+ Name=Convert File to .taf
303
+ Name[de]=Datei zu .taf konvertieren
304
+ Icon={self.icon_path}
305
+ Exec=konsole -e {self.convert_cmd} %f
306
+ """
307
+
308
+ if self.upload_enabled:
309
+ desktop_content += f"""
310
+ [Desktop Action upload]
311
+ Name=Convert File to .taf and Upload
312
+ Name[de]=Datei zu .taf konvertieren und hochladen
313
+ Icon={self.icon_path}
314
+ Exec=konsole -e {self.upload_cmd} %f
315
+
316
+ [Desktop Action upload_artwork]
317
+ Name=Convert File to .taf and Upload + Artwork
318
+ Name[de]=Datei zu .taf konvertieren und hochladen + Artwork
319
+ Icon={self.icon_path}
320
+ Exec=konsole -e {self.upload_artwork_cmd} %f
321
+
322
+ [Desktop Action upload_artwork_json]
323
+ Name=Convert File to .taf and Upload + Artwork + JSON
324
+ Name[de]=Datei zu .taf konvertieren und hochladen + Artwork + JSON
325
+ Icon={self.icon_path}
326
+ Exec=konsole -e {self.upload_artwork_json_cmd} %f
327
+ """
328
+
329
+ return 'tonietoolbox-audio.desktop', desktop_content
330
+
331
+ def _generate_taf_file_entries(self):
332
+ """Generate KDE service menu entries for .taf files"""
333
+ desktop_content = f"""[Desktop Entry]
334
+ Type=Service
335
+ ServiceTypes=KonqPopupMenu/Plugin
336
+ MimeType=audio/x-tonie;
337
+ Actions=show_info;extract_opus;"""
338
+
339
+ if self.upload_enabled:
340
+ desktop_content += "upload_taf;upload_taf_artwork;upload_taf_artwork_json;"
341
+
342
+ desktop_content += f"""
343
+ X-KDE-Submenu=TonieToolbox
344
+ X-KDE-Submenu[de]=TonieToolbox
345
+ Icon={self.icon_path}
346
+
347
+ [Desktop Action show_info]
348
+ Name=Show Info
349
+ Name[de]=Info anzeigen
350
+ Icon={self.icon_path}
351
+ Exec=konsole --noclose -e {self.show_info_cmd} %f
352
+
353
+ [Desktop Action extract_opus]
354
+ Name=Extract Opus Tracks
355
+ Name[de]=Opus-Spuren extrahieren
356
+ Icon={self.icon_path}
357
+ Exec=konsole -e {self.extract_opus_cmd} %f
358
+ """
359
+
360
+ if self.upload_enabled:
361
+ desktop_content += f"""
362
+ [Desktop Action upload_taf]
363
+ Name=Upload
364
+ Name[de]=Hochladen
365
+ Icon={self.icon_path}
366
+ Exec=konsole -e {self.upload_taf_cmd} %f
367
+
368
+ [Desktop Action upload_taf_artwork]
369
+ Name=Upload + Artwork
370
+ Name[de]=Hochladen + Artwork
371
+ Icon={self.icon_path}
372
+ Exec=konsole -e {self.upload_taf_artwork_cmd} %f
373
+
374
+ [Desktop Action upload_taf_artwork_json]
375
+ Name=Upload + Artwork + JSON
376
+ Name[de]=Hochladen + Artwork + JSON
377
+ Icon={self.icon_path}
378
+ Exec=konsole -e {self.upload_taf_artwork_json_cmd} %f
379
+ """
380
+
381
+ return 'tonietoolbox-taf.desktop', desktop_content
382
+
383
+ def _generate_folder_entries(self):
384
+ """Generate KDE service menu entries for folders"""
385
+ desktop_content = f"""[Desktop Entry]
386
+ Type=Service
387
+ ServiceTypes=KonqPopupMenu/Plugin
388
+ MimeType=inode/directory;
389
+ Actions=convert_folder;"""
390
+
391
+ if self.upload_enabled:
392
+ desktop_content += "upload_folder;upload_folder_artwork;upload_folder_artwork_json;"
393
+
394
+ desktop_content += f"""
395
+ X-KDE-Submenu=TonieToolbox
396
+ X-KDE-Submenu[de]=TonieToolbox
397
+ Icon={self.icon_path}
398
+
399
+ [Desktop Action convert_folder]
400
+ Name=Convert Folder to .taf (recursive)
401
+ Name[de]=Ordner zu .taf konvertieren (rekursiv)
402
+ Icon={self.icon_path}
403
+ Exec=konsole -e {self.convert_folder_cmd} %f
404
+ """
405
+
406
+ if self.upload_enabled:
407
+ desktop_content += f"""
408
+ [Desktop Action upload_folder]
409
+ Name=Convert Folder to .taf and Upload (recursive)
410
+ Name[de]=Ordner zu .taf konvertieren und hochladen (rekursiv)
411
+ Icon={self.icon_path}
412
+ Exec=konsole -e {self.upload_folder_cmd} %f
413
+
414
+ [Desktop Action upload_folder_artwork]
415
+ Name=Convert Folder to .taf and Upload + Artwork (recursive)
416
+ Name[de]=Ordner zu .taf konvertieren und hochladen + Artwork (rekursiv)
417
+ Icon={self.icon_path}
418
+ Exec=konsole -e {self.upload_folder_artwork_cmd} %f
419
+
420
+ [Desktop Action upload_folder_artwork_json]
421
+ Name=Convert Folder to .taf and Upload + Artwork + JSON (recursive)
422
+ Name[de]=Ordner zu .taf konvertieren und hochladen + Artwork + JSON (rekursiv)
423
+ Icon={self.icon_path}
424
+ Exec=konsole -e {self.upload_folder_artwork_json_cmd} %f
425
+ """
426
+
427
+ return 'tonietoolbox-folder.desktop', desktop_content
428
+
429
+ def generate_service_menu_files(self):
430
+ """
431
+ Generate KDE service menu files for TonieToolbox integration.
432
+ Returns the paths to the generated service menu files.
433
+ """
434
+ os.makedirs(self.service_menu_dir, exist_ok=True)
435
+ generated_files = []
436
+
437
+ # Generate entries for audio extensions
438
+ filename, content = self._generate_audio_extensions_entries()
439
+ file_path = os.path.join(self.service_menu_dir, filename)
440
+ with open(file_path, 'w', encoding='utf-8') as f:
441
+ f.write(content)
442
+ os.chmod(file_path, 0o755) # Make executable
443
+ generated_files.append(file_path)
444
+
445
+ # Generate entries for .taf files
446
+ filename, content = self._generate_taf_file_entries()
447
+ file_path = os.path.join(self.service_menu_dir, filename)
448
+ with open(file_path, 'w', encoding='utf-8') as f:
449
+ f.write(content)
450
+ os.chmod(file_path, 0o755) # Make executable
451
+ generated_files.append(file_path)
452
+
453
+ # Generate entries for folders
454
+ filename, content = self._generate_folder_entries()
455
+ file_path = os.path.join(self.service_menu_dir, filename)
456
+ with open(file_path, 'w', encoding='utf-8') as f:
457
+ f.write(content)
458
+ os.chmod(file_path, 0o755) # Make executable
459
+ generated_files.append(file_path)
460
+
461
+ return generated_files
462
+
463
+ def _generate_application_desktop_file(self):
464
+ """Generate desktop application file for opening .taf files with double-click"""
465
+ desktop_content = f"""[Desktop Entry]
466
+ Version=1.0
467
+ Type=Application
468
+ Name=TonieToolbox Player
469
+ Name[de]=TonieToolbox Player
470
+ Comment=Play Tonie audio files with TonieToolbox
471
+ Comment[de]=Tonie-Audiodateien mit TonieToolbox abspielen
472
+ GenericName=Tonie Audio Player
473
+ GenericName[de]=Tonie Audio Player
474
+ Exec=konsole -e {self.play_cmd} %f
475
+ Icon={self.icon_path}
476
+ Terminal=false
477
+ NoDisplay=true
478
+ MimeType=audio/x-tonie;
479
+ Categories=AudioVideo;Audio;Player;
480
+ """
481
+ return 'tonietoolbox-player.desktop', desktop_content
482
+
483
+ def generate_application_file(self):
484
+ """Generate and install the desktop application file"""
485
+ os.makedirs(self.application_dir, exist_ok=True)
486
+
487
+ filename, content = self._generate_application_desktop_file()
488
+ file_path = os.path.join(self.application_dir, filename)
489
+ with open(file_path, 'w', encoding='utf-8') as f:
490
+ f.write(content)
491
+ os.chmod(file_path, 0o755) # Make executable
492
+
493
+ return file_path
494
+
495
+ def remove_service_menu_files(self):
496
+ """
497
+ Remove TonieToolbox service menu files.
498
+ """
499
+ files_to_remove = [
500
+ 'tonietoolbox-audio.desktop',
501
+ 'tonietoolbox-taf.desktop',
502
+ 'tonietoolbox-folder.desktop'
503
+ ]
504
+
505
+ removed_files = []
506
+ for filename in files_to_remove:
507
+ file_path = os.path.join(self.service_menu_dir, filename)
508
+ if os.path.exists(file_path):
509
+ try:
510
+ os.remove(file_path)
511
+ removed_files.append(file_path)
512
+ logger.info(f"Removed service menu file: {file_path}")
513
+ except Exception as e:
514
+ logger.error(f"Failed to remove service menu file {file_path}: {e}")
515
+
516
+ return removed_files
517
+
518
+ def remove_application_file(self):
519
+ """Remove the TonieToolbox player application desktop file"""
520
+ app_filename = 'tonietoolbox-player.desktop'
521
+ app_file_path = os.path.join(self.application_dir, app_filename)
522
+
523
+ if os.path.exists(app_file_path):
524
+ try:
525
+ os.remove(app_file_path)
526
+ logger.info(f"Removed application file: {app_file_path}")
527
+ return app_file_path
528
+ except Exception as e:
529
+ logger.error(f"Failed to remove application file {app_file_path}: {e}")
530
+ return None
531
+
532
+ def create_taf_mime_type(self):
533
+ """Create MIME type definition for .taf files"""
534
+ mime_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', 'mime')
535
+ packages_dir = os.path.join(mime_dir, 'packages')
536
+ os.makedirs(packages_dir, exist_ok=True)
537
+
538
+ # Create MIME type definition with higher priority for file extension
539
+ mime_xml_content = '''<?xml version="1.0" encoding="UTF-8"?>
540
+ <mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
541
+ <mime-type type="audio/x-tonie">
542
+ <comment>Tonie Audio File</comment>
543
+ <comment xml:lang="de">Tonie-Audiodatei</comment>
544
+ <icon name="audio-x-generic"/>
545
+ <glob pattern="*.taf" weight="100"/>
546
+ <glob pattern="*.TAF" weight="100"/>
547
+ </mime-type>
548
+ </mime-info>'''
549
+
550
+ mime_xml_path = os.path.join(packages_dir, 'audio-x-tonie.xml')
551
+ with open(mime_xml_path, 'w', encoding='utf-8') as f:
552
+ f.write(mime_xml_content)
553
+
554
+ # Update MIME database
555
+ import subprocess
556
+ try:
557
+ subprocess.run(['update-mime-database', mime_dir], check=True, capture_output=True)
558
+ logger.info(f"Created MIME type definition: {mime_xml_path}")
559
+ return mime_xml_path
560
+ except subprocess.CalledProcessError as e:
561
+ logger.error(f"Failed to update MIME database: {e}")
562
+ return None
563
+ except FileNotFoundError:
564
+ logger.warning("update-mime-database command not found")
565
+ return None
566
+
567
+ def remove_taf_mime_type(self):
568
+ """Remove MIME type definition for .taf files"""
569
+ mime_dir = os.path.join(os.path.expanduser('~'), '.local', 'share', 'mime')
570
+ mime_xml_path = os.path.join(mime_dir, 'packages', 'audio-x-tonie.xml')
571
+
572
+ if os.path.exists(mime_xml_path):
573
+ try:
574
+ os.remove(mime_xml_path)
575
+ # Update MIME database
576
+ import subprocess
577
+ subprocess.run(['update-mime-database', mime_dir], check=True, capture_output=True)
578
+ logger.info(f"Removed MIME type definition: {mime_xml_path}")
579
+ return mime_xml_path
580
+ except Exception as e:
581
+ logger.error(f"Failed to remove MIME type definition: {e}")
582
+ return None
583
+
584
+ def _update_kde_cache(self):
585
+ """Update KDE's service menu cache."""
586
+ import subprocess
587
+ cache_updated = False
588
+
589
+ # KDE6 cache update
590
+ try:
591
+ result = subprocess.run(['kbuildsycoca6'], check=False, capture_output=True, text=True)
592
+ if result.returncode == 0:
593
+ logger.info("Updated KDE6 service menu cache")
594
+ cache_updated = True
595
+ else:
596
+ logger.debug(f"kbuildsycoca6 failed: {result.stderr}")
597
+ except FileNotFoundError:
598
+ pass
599
+
600
+ # KDE5 cache update (fallback)
601
+ if not cache_updated:
602
+ try:
603
+ result = subprocess.run(['kbuildsycoca5'], check=False, capture_output=True, text=True)
604
+ if result.returncode == 0:
605
+ logger.info("Updated KDE5 service menu cache")
606
+ cache_updated = True
607
+ else:
608
+ logger.debug(f"kbuildsycoca5 failed: {result.stderr}")
609
+ except FileNotFoundError:
610
+ pass
611
+
612
+ if not cache_updated:
613
+ logger.warning("Could not find kbuildsycoca command. Service menus may require logout/login to appear.")
614
+ logger.info("You can try manually running: kbuildsycoca6 or kbuildsycoca5")
615
+
616
+ @classmethod
617
+ def install(cls):
618
+ """
619
+ Generate service menu files and install them.
620
+ """
621
+ try:
622
+ instance = cls()
623
+ # Create MIME type definition for .taf files
624
+ mime_file = instance.create_taf_mime_type()
625
+ generated_files = instance.generate_service_menu_files()
626
+ app_file = instance.generate_application_file()
627
+ generated_files.append(app_file)
628
+ if mime_file:
629
+ generated_files.append(mime_file)
630
+ instance._update_kde_cache()
631
+ logger.info(f"KDE integration installed successfully. Generated files: {generated_files}")
632
+ print(f"KDE integration installed successfully!")
633
+ print(f"Generated service menu files:")
634
+ for file_path in generated_files:
635
+ print(f" {file_path}")
636
+ print("Service menus should appear in the right-click context menu.")
637
+ print("Double-click .taf files will now play them with TonieToolbox.")
638
+ print("")
639
+ print("Note: If the menus don't appear immediately:")
640
+ print(" 1. Try restarting Dolphin file manager")
641
+ print(" 2. Or log out and back in to refresh all KDE services")
642
+ print(" 3. For immediate effect, you can run: kbuildsycoca6")
643
+ return True
644
+ except Exception as e:
645
+ logger.error(f"KDE integration installation failed: {e}")
646
+ print(f"KDE integration installation failed: {e}")
647
+ return False
648
+
649
+ @classmethod
650
+ def uninstall(cls):
651
+ """
652
+ Remove service menu files and uninstall integration.
653
+ """
654
+ try:
655
+ instance = cls()
656
+ removed_files = instance.remove_service_menu_files()
657
+ app_file = instance.remove_application_file()
658
+ if app_file:
659
+ removed_files.append(app_file)
660
+ # Remove MIME type definition
661
+ mime_file = instance.remove_taf_mime_type()
662
+ if mime_file:
663
+ removed_files.append(mime_file)
664
+ instance._update_kde_cache()
665
+ logger.info(f"KDE integration uninstalled successfully. Removed files: {removed_files}")
666
+ print(f"KDE integration uninstalled successfully!")
667
+ if removed_files:
668
+ print(f"Removed service menu files:")
669
+ for file_path in removed_files:
670
+ print(f" {file_path}")
671
+ else:
672
+ print("No service menu files were found to remove.")
673
+ return True
674
+ except Exception as e:
675
+ logger.error(f"KDE integration uninstallation failed: {e}")
676
+ print(f"KDE integration uninstallation failed: {e}")
677
+ return False
@@ -177,8 +177,10 @@ class WindowsClassicContextMenuIntegration:
177
177
  self.basic_authentication = True
178
178
  self.client_cert_path = upload_config.get('client_cert_path', '')
179
179
  self.client_cert_key_path = upload_config.get('client_cert_key_path', '')
180
- if self.client_cert_path and self.client_cert_key_path:
181
- self.client_cert_cmd = f'--client-cert {self.client_cert_path} --client-cert-key {self.client_cert_key_path}'
180
+ if self.client_cert_path and self.client_cert_key_path: # Escape paths for registry use (double backslashes)
181
+ cert_path_escaped = self.client_cert_path.replace('\\', '\\\\')
182
+ key_path_escaped = self.client_cert_key_path.replace('\\', '\\\\')
183
+ self.client_cert_cmd = f'--client-cert "{cert_path_escaped}" --client-key "{key_path_escaped}"'
182
184
  self.client_cert_authentication = True
183
185
  if self.client_cert_authentication and self.basic_authentication:
184
186
  logger.warning("Both client certificate and basic authentication are set. Only one can be used.")