TonieToolbox 0.6.1__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.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +218 -21
- TonieToolbox/audio_conversion.py +77 -0
- TonieToolbox/dependency_manager.py +60 -0
- TonieToolbox/integration.py +37 -1
- TonieToolbox/integration_kde.py +677 -0
- TonieToolbox/integration_windows.py +4 -2
- TonieToolbox/media_tags.py +95 -0
- TonieToolbox/player.py +638 -0
- TonieToolbox/recursive_processor.py +29 -1
- TonieToolbox/teddycloud.py +79 -4
- TonieToolbox/tonie_analysis.py +235 -1
- {tonietoolbox-0.6.1.dist-info → tonietoolbox-0.6.4.dist-info}/METADATA +7 -1
- tonietoolbox-0.6.4.dist-info/RECORD +32 -0
- {tonietoolbox-0.6.1.dist-info → tonietoolbox-0.6.4.dist-info}/WHEEL +1 -1
- tonietoolbox-0.6.1.dist-info/RECORD +0 -30
- {tonietoolbox-0.6.1.dist-info → tonietoolbox-0.6.4.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.6.1.dist-info → tonietoolbox-0.6.4.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.6.1.dist-info → tonietoolbox-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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.")
|