TonieToolbox 0.5.1__py3-none-any.whl → 0.6.0a2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
TonieToolbox/__init__.py CHANGED
@@ -2,4 +2,4 @@
2
2
  TonieToolbox - Convert audio files to Tonie box compatible format
3
3
  """
4
4
 
5
- __version__ = '0.5.1'
5
+ __version__ = '0.6.0a2'
TonieToolbox/__main__.py CHANGED
@@ -21,6 +21,7 @@ from .teddycloud import TeddyCloudClient
21
21
  from .tags import get_tags
22
22
  from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
23
23
  from .artwork import upload_artwork
24
+ from .integration import handle_integration, handle_config
24
25
 
25
26
  def main():
26
27
  """Entry point for the TonieToolbox application."""
@@ -98,7 +99,14 @@ def main():
98
99
  parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
99
100
  help='Compare input file with another .taf file for debugging')
100
101
  parser.add_argument('-D', '--detailed-compare', action='store_true',
101
- help='Show detailed OGG page differences when comparing files')
102
+ help='Show detailed OGG page differences when comparing files')
103
+ # ------------- Parser - Context Menu Integration -------------
104
+ parser.add_argument('--config-integration', action='store_true',
105
+ help='Configure context menu integration')
106
+ parser.add_argument('--install-integration', action='store_true',
107
+ help='Integrate with the system (e.g., create context menu entries)')
108
+ parser.add_argument('--uninstall-integration', action='store_true',
109
+ help='Uninstall context menu integration')
102
110
  # ------------- Parser - Media Tag Options -------------
103
111
  media_tag_group = parser.add_argument_group('Media Tag Options')
104
112
  media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
@@ -127,7 +135,7 @@ def main():
127
135
  args = parser.parse_args()
128
136
 
129
137
  # ------------- Parser - Source Input -------------
130
- if args.input_filename is None and not (args.get_tags or args.upload):
138
+ if args.input_filename is None and not (args.get_tags or args.upload or args.install_integration or args.uninstall_integration or args.config_integration):
131
139
  parser.error("the following arguments are required: SOURCE")
132
140
 
133
141
  # ------------- Logging -------------
@@ -166,7 +174,23 @@ def main():
166
174
 
167
175
  if not is_latest and not update_confirmed and not (args.silent or args.quiet):
168
176
  logger.info("Update available but user chose to continue without updating.")
169
-
177
+ # ------------- Context Menu Integration -------------
178
+ if args.install_integration or args.uninstall_integration:
179
+ logger.debug("Context menu integration requested: install=%s, uninstall=%s",
180
+ args.install_integration, args.uninstall_integration)
181
+ success = handle_integration(args)
182
+ if success:
183
+ if args.install_integration:
184
+ logger.info("Context menu integration installed successfully")
185
+ else:
186
+ logger.info("Context menu integration uninstalled successfully")
187
+ else:
188
+ logger.error("Failed to handle context menu integration")
189
+ sys.exit(0)
190
+ if args.config_integration:
191
+ logger.debug("Opening configuration file for editing")
192
+ handle_config()
193
+ sys.exit(0)
170
194
  # ------------- Normalize Path Input -------------
171
195
  if args.input_filename:
172
196
  logger.debug("Original input path: %s", args.input_filename)
@@ -224,89 +248,72 @@ def main():
224
248
  print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
225
249
  sys.exit(0)
226
250
  # ------------- Direct Upload -------------
227
- if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
228
- file_path = args.input_filename
229
- file_size = os.path.getsize(file_path)
230
- file_ext = os.path.splitext(file_path)[1].lower()
231
-
232
- if args.upload and not args.recursive and file_ext == '.taf':
233
- logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
234
- logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
235
- args.path, args.special_folder, args.ignore_ssl_verify)
236
-
251
+ if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
252
+ file_path = args.input_filename
253
+ file_size = os.path.getsize(file_path)
254
+ file_ext = os.path.splitext(file_path)[1].lower()
237
255
 
238
- logger.debug("File to upload: %s (size: %d bytes, type: %s)",
239
- file_path, file_size, file_ext)
240
- logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
241
- logger.trace("Starting upload process for %s", file_path)
242
- response = client.upload_file(
243
- destination_path=args.path,
244
- file_path=file_path,
245
- special=args.special_folder,
246
- )
247
- logger.trace("Upload response received: %s", response)
248
- upload_success = response.get('success', False)
249
- if not upload_success:
250
- error_msg = response.get('message', 'Unknown error')
251
- logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
252
- file_path, error_msg, response.get('status_code', 'Unknown'), response)
253
- logger.trace("Exiting with code 1 due to upload failure")
254
- sys.exit(1)
255
- else:
256
- logger.info("Successfully uploaded %s to TeddyCloud", file_path)
257
- logger.debug("Upload response details: %s",
258
- {k: v for k, v in response.items() if k != 'success'})
259
- artwork_url = None
260
- if args.include_artwork and file_path.lower().endswith('.taf'):
261
- source_dir = os.path.dirname(file_path)
262
- logger.info("Looking for artwork to upload for %s", file_path)
263
- logger.debug("Searching for artwork in directory: %s", source_dir)
264
- logger.trace("Calling upload_artwork function")
265
- success, artwork_url = upload_artwork(client, file_path, source_dir, [])
266
- logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
267
- success, artwork_url)
268
- if success:
269
- logger.info("Successfully uploaded artwork for %s", file_path)
270
- logger.debug("Artwork URL: %s", artwork_url)
271
- else:
272
- logger.warning("Failed to upload artwork for %s", file_path)
273
- logger.debug("No suitable artwork found or upload failed")
274
- if args.create_custom_json and file_path.lower().endswith('.taf'):
275
- output_dir = './output'
276
- logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
277
- if not os.path.exists(output_dir):
278
- os.makedirs(output_dir, exist_ok=True)
279
- logger.trace("Created output directory: %s", output_dir)
280
- logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
281
- file_path, artwork_url)
282
- client_param = client
283
-
284
- if args.version_2:
285
- logger.debug("Using version 2 of the Tonies JSON format")
286
- success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
287
- else:
288
- success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
289
- if success:
290
- logger.info("Successfully updated Tonies JSON for %s", file_path)
291
- else:
292
- logger.warning("Failed to update Tonies JSON for %s", file_path)
293
- logger.debug("fetch_and_update_tonies_json returned failure")
294
- logger.trace("Exiting after direct upload with code 0")
295
- sys.exit(0)
296
- elif not args.recursive:
297
- if not os.path.exists(args.input_filename):
298
- logger.error("File not found: %s", args.input_filename)
299
- elif not os.path.isfile(args.input_filename):
300
- logger.error("Not a regular file: %s", args.input_filename)
301
- logger.debug("File exists: %s, Is file: %s",
302
- os.path.exists(args.input_filename),
303
- os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
304
- logger.trace("Exiting with code 1 due to invalid input file")
256
+ if args.upload and not args.recursive and file_ext == '.taf':
257
+ logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
258
+ logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
259
+ args.path, args.special_folder, args.ignore_ssl_verify)
260
+ logger.debug("File to upload: %s (size: %d bytes, type: %s)",
261
+ file_path, file_size, file_ext)
262
+ logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
263
+ logger.trace("Starting upload process for %s", file_path)
264
+ response = client.upload_file(
265
+ destination_path=args.path,
266
+ file_path=file_path,
267
+ special=args.special_folder,
268
+ )
269
+ logger.trace("Upload response received: %s", response)
270
+ upload_success = response.get('success', False)
271
+ if not upload_success:
272
+ error_msg = response.get('message', 'Unknown error')
273
+ logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
274
+ file_path, error_msg, response.get('status_code', 'Unknown'), response)
275
+ logger.trace("Exiting with code 1 due to upload failure")
305
276
  sys.exit(1)
306
-
307
- if args.recursive and args.upload:
308
- logger.info("Recursive mode with upload enabled: %s -> %s", args.input_filename, teddycloud_url)
309
- logger.debug("Will process all files in directory recursively and upload to TeddyCloud")
277
+ else:
278
+ logger.info("Successfully uploaded %s to TeddyCloud", file_path)
279
+ logger.debug("Upload response details: %s",
280
+ {k: v for k, v in response.items() if k != 'success'})
281
+ artwork_url = None
282
+ if args.include_artwork and file_path.lower().endswith('.taf'):
283
+ source_dir = os.path.dirname(file_path)
284
+ logger.info("Looking for artwork to upload for %s", file_path)
285
+ logger.debug("Searching for artwork in directory: %s", source_dir)
286
+ logger.trace("Calling upload_artwork function")
287
+ success, artwork_url = upload_artwork(client, file_path, source_dir, [])
288
+ logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
289
+ success, artwork_url)
290
+ if success:
291
+ logger.info("Successfully uploaded artwork for %s", file_path)
292
+ logger.debug("Artwork URL: %s", artwork_url)
293
+ else:
294
+ logger.warning("Failed to upload artwork for %s", file_path)
295
+ logger.debug("No suitable artwork found or upload failed")
296
+ if args.create_custom_json and file_path.lower().endswith('.taf'):
297
+ output_dir = './output'
298
+ logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
299
+ if not os.path.exists(output_dir):
300
+ os.makedirs(output_dir, exist_ok=True)
301
+ logger.trace("Created output directory: %s", output_dir)
302
+ logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
303
+ file_path, artwork_url)
304
+ client_param = client
305
+ if args.version_2:
306
+ logger.debug("Using version 2 of the Tonies JSON format")
307
+ success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
308
+ else:
309
+ success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
310
+ if success:
311
+ logger.info("Successfully updated Tonies JSON for %s", file_path)
312
+ else:
313
+ logger.warning("Failed to update Tonies JSON for %s", file_path)
314
+ logger.debug("fetch_and_update_tonies_json returned failure")
315
+ logger.trace("Exiting after direct upload with code 0")
316
+ sys.exit(0)
310
317
 
311
318
  # ------------- Librarys / Prereqs -------------
312
319
  logger.debug("Checking for external dependencies")
TonieToolbox/artwork.py CHANGED
@@ -13,17 +13,22 @@ from .teddycloud import TeddyCloudClient
13
13
  from .media_tags import extract_artwork, find_cover_image
14
14
 
15
15
 
16
- def upload_artwork(client: TeddyCloudClient, taf_filename, source_path, audio_files) -> Tuple[bool, Optional[str]]:
16
+ def upload_artwork(
17
+ client: TeddyCloudClient,
18
+ taf_filename: str,
19
+ source_path: str,
20
+ audio_files: list[str],
21
+ ) -> tuple[bool, Optional[str]]:
17
22
  """
18
23
  Find and upload artwork for a Tonie file.
19
-
24
+
20
25
  Args:
21
- client: TeddyCloudClient instance to use for API communication
22
- taf_filename: The filename of the Tonie file (.taf)
23
- source_path: Source directory to look for artwork
24
- audio_files: List of audio files to extract artwork from if needed
26
+ client (TeddyCloudClient): TeddyCloudClient instance to use for API communication
27
+ taf_filename (str): The filename of the Tonie file (.taf)
28
+ source_path (str): Source directory to look for artwork
29
+ audio_files (list[str]): List of audio files to extract artwork from if needed
25
30
  Returns:
26
- tuple: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
31
+ tuple[bool, Optional[str]]: (success, artwork_url) where success is a boolean and artwork_url is the URL of the uploaded artwork
27
32
  """
28
33
  logger = get_logger('artwork')
29
34
  logger.info("Looking for artwork for Tonie file: %s", taf_filename)
@@ -7,27 +7,36 @@ import glob
7
7
  import subprocess
8
8
  import tempfile
9
9
  from .dependency_manager import get_ffmpeg_binary, get_opus_binary
10
+ from .constants import SUPPORTED_EXTENSIONS
10
11
  from .logger import get_logger
11
12
 
12
13
  logger = get_logger('audio_conversion')
13
14
 
14
15
 
15
- def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitrate=48, vbr=True, keep_temp=False, auto_download=False, no_mono_conversion=False):
16
+ def get_opus_tempfile(
17
+ ffmpeg_binary: str = None,
18
+ opus_binary: str = None,
19
+ filename: str = None,
20
+ bitrate: int = 48,
21
+ vbr: bool = True,
22
+ keep_temp: bool = False,
23
+ auto_download: bool = False,
24
+ no_mono_conversion: bool = False
25
+ ) -> tuple[tempfile.SpooledTemporaryFile | None, str | None]:
16
26
  """
17
27
  Convert an audio file to Opus format and return a temporary file handle.
18
28
 
19
29
  Args:
20
- ffmpeg_binary: Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
21
- opus_binary: Path to the opusenc binary. If None, will be auto-detected or downloaded.
22
- filename: Path to the input audio file
23
- bitrate: Bitrate for the Opus encoding in kbps
24
- vbr: Whether to use variable bitrate encoding
25
- keep_temp: Whether to keep the temporary files for testing
26
- auto_download: Whether to automatically download dependencies if not found
27
- no_mono_conversion: Whether to skip mono to stereo conversion
28
-
30
+ ffmpeg_binary (str | None): Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
31
+ opus_binary (str | None): Path to the opusenc binary. If None, will be auto-detected or downloaded.
32
+ filename (str | None): Path to the input audio file
33
+ bitrate (int): Bitrate for the Opus encoding in kbps
34
+ vbr (bool): Whether to use variable bitrate encoding
35
+ keep_temp (bool): Whether to keep the temporary files for testing
36
+ auto_download (bool): Whether to automatically download dependencies if not found
37
+ no_mono_conversion (bool): Whether to skip mono to stereo conversion
29
38
  Returns:
30
- tuple: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
39
+ tuple[tempfile.SpooledTemporaryFile | None, str | None]: (file handle, temp_file_path) or (file handle, None) if keep_temp is False
31
40
  """
32
41
  logger.trace("Entering get_opus_tempfile(ffmpeg_binary=%s, opus_binary=%s, filename=%s, bitrate=%d, vbr=%s, keep_temp=%s, auto_download=%s, no_mono_conversion=%s)",
33
42
  ffmpeg_binary, opus_binary, filename, bitrate, vbr, keep_temp, auto_download, no_mono_conversion)
@@ -198,24 +207,20 @@ def get_opus_tempfile(ffmpeg_binary=None, opus_binary=None, filename=None, bitra
198
207
  return tmp_file, None
199
208
 
200
209
 
201
- def filter_directories(glob_list):
210
+ def filter_directories(glob_list: list[str]) -> list[str]:
202
211
  """
203
212
  Filter a list of glob results to include only audio files that can be handled by ffmpeg.
204
213
 
205
214
  Args:
206
- glob_list: List of path names from glob.glob()
207
-
215
+ glob_list (list[str]): List of path names from glob.glob()
208
216
  Returns:
209
- list: Filtered list containing only supported audio files
217
+ list[str]: Filtered list containing only supported audio files
210
218
  """
211
219
  logger.trace("Entering filter_directories() with %d items", len(glob_list))
212
220
  logger.debug("Filtering %d glob results for supported audio files", len(glob_list))
213
221
 
214
- # Common audio file extensions supported by ffmpeg
215
- supported_extensions = [
216
- '.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
217
- '.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
218
- ]
222
+ supported_extensions = SUPPORTED_EXTENSIONS
223
+ logger.debug("Supported audio file extensions: %s", supported_extensions)
219
224
 
220
225
  filtered = []
221
226
  for name in glob_list:
@@ -232,17 +237,16 @@ def filter_directories(glob_list):
232
237
  return filtered
233
238
 
234
239
 
235
- def get_input_files(input_filename):
240
+ def get_input_files(input_filename: str) -> list[str]:
236
241
  """
237
242
  Get a list of input files to process.
238
243
 
239
244
  Supports direct file paths, directory paths, glob patterns, and .lst files.
240
245
 
241
246
  Args:
242
- input_filename: Input file pattern or list file path
243
-
247
+ input_filename (str): Input file pattern or list file path
244
248
  Returns:
245
- list: List of input file paths
249
+ list[str]: List of input file paths
246
250
  """
247
251
  logger.trace("Entering get_input_files(input_filename=%s)", input_filename)
248
252
  logger.debug("Getting input files for pattern: %s", input_filename)
@@ -320,14 +324,13 @@ def get_input_files(input_filename):
320
324
  return input_files
321
325
 
322
326
 
323
- def append_to_filename(output_filename, tag):
327
+ def append_to_filename(output_filename: str, tag: str) -> str:
324
328
  """
325
329
  Append a tag to a filename, preserving the extension.
326
330
 
327
331
  Args:
328
- output_filename: Original filename
329
- tag: Tag to append (typically an 8-character hex value)
330
-
332
+ output_filename (str): Original filename
333
+ tag (str): Tag to append (typically an 8-character hex value)
331
334
  Returns:
332
335
  str: Modified filename with tag
333
336
  """
TonieToolbox/constants.py CHANGED
@@ -1,13 +1,13 @@
1
1
  """
2
2
  Constants used throughout the TonieToolbox package
3
3
  """
4
- SAMPLE_RATE_KHZ = 48
5
- ONLY_CONVERT_FRAMEPACKING = -1
6
- OTHER_PACKET_NEEDED = -2
7
- DO_NOTHING = -3
8
- TOO_MANY_SEGMENTS = -4
9
- TIMESTAMP_DEDUCT = 0x50000000
10
- OPUS_TAGS = [
4
+ SAMPLE_RATE_KHZ: int = 48
5
+ ONLY_CONVERT_FRAMEPACKING: int = -1
6
+ OTHER_PACKET_NEEDED: int = -2
7
+ DO_NOTHING: int = -3
8
+ TOO_MANY_SEGMENTS: int = -4
9
+ TIMESTAMP_DEDUCT: int = 0x50000000
10
+ OPUS_TAGS: list[bytearray] = [
11
11
  bytearray(
12
12
  b"\x4F\x70\x75\x73\x54\x61\x67\x73\x0D\x00\x00\x00\x4C\x61\x76\x66\x35\x38\x2E\x32\x30\x2E\x31\x30\x30\x03\x00\x00\x00\x26\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x3D\x6F\x70\x75\x73\x65\x6E\x63\x20\x66\x72\x6F\x6D\x20\x6F\x70\x75\x73\x2D\x74\x6F\x6F\x6C\x73\x20\x30\x2E\x31\x2E\x31\x30\x2A\x00\x00\x00\x65\x6E\x63\x6F\x64\x65\x72\x5F\x6F\x70\x74\x69\x6F\x6E\x73\x3D\x2D\x2D\x71\x75\x69\x65\x74\x20\x2D\x2D\x62\x69\x74\x72\x61\x74\x65\x20\x39\x36\x20\x2D\x2D\x76\x62\x72\x3B\x01\x00\x00\x70\x61\x64\x3D\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30\x30"),
13
13
  bytearray(
@@ -15,7 +15,7 @@ OPUS_TAGS = [
15
15
  ]
16
16
 
17
17
  # Mapping of language tags to ISO codes
18
- LANGUAGE_MAPPING = {
18
+ LANGUAGE_MAPPING: dict[str, str] = {
19
19
  # Common language names to ISO codes
20
20
  'deutsch': 'de-de',
21
21
  'german': 'de-de',
@@ -39,7 +39,7 @@ LANGUAGE_MAPPING = {
39
39
  }
40
40
 
41
41
  # Mapping of genre tags to tonie categories
42
- GENRE_MAPPING = {
42
+ GENRE_MAPPING: dict[str, str] = {
43
43
  # Standard Tonie category names from tonies.json
44
44
  'hörspiel': 'Hörspiele & Hörbücher',
45
45
  'hörbuch': 'Hörspiele & Hörbücher',
@@ -87,4 +87,105 @@ GENRE_MAPPING = {
87
87
 
88
88
  # Default to standard format for custom
89
89
  'custom': 'Hörspiele & Hörbücher',
90
+ }
91
+
92
+ # Supported file extensions for audio files
93
+ SUPPORTED_EXTENSIONS = [
94
+ '.wav', '.mp3', '.aac', '.m4a', '.flac', '.ogg', '.opus',
95
+ '.ape', '.wma', '.aiff', '.mp2', '.mp4', '.webm', '.mka'
96
+ ]
97
+
98
+ ARTWORK_NAMES = [
99
+ 'cover', 'folder', 'album', 'front', 'artwork', 'image',
100
+ 'albumart', 'albumartwork', 'booklet'
101
+ ]
102
+ ARTWORK_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.bmp', '.gif']
103
+
104
+
105
+ TAG_VALUE_REPLACEMENTS = {
106
+ "Die drei ???": "Die drei Fragezeichen",
107
+ "Die Drei ???": "Die drei Fragezeichen",
108
+ "DIE DREI ???": "Die drei Fragezeichen",
109
+ "Die drei !!!": "Die drei Ausrufezeichen",
110
+ "Die Drei !!!": "Die drei Ausrufezeichen",
111
+ "DIE DREI !!!": "Die drei Ausrufezeichen",
112
+ "TKKG™": "TKKG",
113
+ "Die drei ??? Kids": "Die drei Fragezeichen Kids",
114
+ "Die Drei ??? Kids": "Die drei Fragezeichen Kids",
115
+ "Bibi & Tina": "Bibi und Tina",
116
+ "Benjamin Blümchen™": "Benjamin Blümchen",
117
+ "???": "Fragezeichen",
118
+ "!!!": "Ausrufezeichen",
119
+ }
120
+
121
+ TAG_MAPPINGS = {
122
+ # ID3 (MP3) tags
123
+ 'TIT2': 'title',
124
+ 'TALB': 'album',
125
+ 'TPE1': 'artist',
126
+ 'TPE2': 'albumartist',
127
+ 'TCOM': 'composer',
128
+ 'TRCK': 'tracknumber',
129
+ 'TPOS': 'discnumber',
130
+ 'TDRC': 'date',
131
+ 'TCON': 'genre',
132
+ 'TPUB': 'publisher',
133
+ 'TCOP': 'copyright',
134
+ 'COMM': 'comment',
135
+
136
+ # Vorbis tags (FLAC, OGG)
137
+ 'title': 'title',
138
+ 'album': 'album',
139
+ 'artist': 'artist',
140
+ 'albumartist': 'albumartist',
141
+ 'composer': 'composer',
142
+ 'tracknumber': 'tracknumber',
143
+ 'discnumber': 'discnumber',
144
+ 'date': 'date',
145
+ 'genre': 'genre',
146
+ 'publisher': 'publisher',
147
+ 'copyright': 'copyright',
148
+ 'comment': 'comment',
149
+
150
+ # MP4 (M4A, AAC) tags
151
+ '©nam': 'title',
152
+ '©alb': 'album',
153
+ '©ART': 'artist',
154
+ 'aART': 'albumartist',
155
+ '©wrt': 'composer',
156
+ 'trkn': 'tracknumber',
157
+ 'disk': 'discnumber',
158
+ '©day': 'date',
159
+ '©gen': 'genre',
160
+ '©pub': 'publisher',
161
+ 'cprt': 'copyright',
162
+ '©cmt': 'comment',
163
+
164
+ # Additional tags some files might have
165
+ 'album_artist': 'albumartist',
166
+ 'track': 'tracknumber',
167
+ 'track_number': 'tracknumber',
168
+ 'disc': 'discnumber',
169
+ 'disc_number': 'discnumber',
170
+ 'year': 'date',
171
+ 'albuminterpret': 'albumartist', # German tag name
172
+ 'interpret': 'artist', # German tag name
173
+
174
+ }
175
+
176
+ CONFIG_TEMPLATE = {
177
+ "metadata": {
178
+ "description": "TonieToolbox configuration",
179
+ "config_version": "1.0"
180
+ },
181
+ "log_level": "silent", # Options: trace, debug, info, warning, error, critical, silent
182
+ "log_to_file": False, # True if you want to log to a file ~\.tonietoolbox\logs
183
+ "upload": {
184
+ "url": [""], # https://teddycloud.example.com
185
+ "ignore_ssl_verify": False, # True if you want to ignore SSL certificate verification
186
+ "username": "", # Basic Auth username
187
+ "password": "", # Basic Auth password
188
+ "client_cert_path": "", # Path to client certificate file
189
+ "client_cert_key_path": "" # Path to client certificate key file
190
+ }
90
191
  }
@@ -15,10 +15,9 @@ def sanitize_filename(filename: str) -> str:
15
15
  Sanitize a filename by removing invalid characters and trimming.
16
16
 
17
17
  Args:
18
- filename: The filename to sanitize
19
-
18
+ filename (str): The filename to sanitize
20
19
  Returns:
21
- A sanitized filename
20
+ str: A sanitized filename
22
21
  """
23
22
  # Remove invalid characters for filenames
24
23
  sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
@@ -29,7 +28,7 @@ def sanitize_filename(filename: str) -> str:
29
28
  return "tonie"
30
29
  return sanitized
31
30
 
32
- def guess_output_filename(input_filename: str, input_files: List[str] = None) -> str:
31
+ def guess_output_filename(input_filename: str, input_files: list[str] = None) -> str:
33
32
  """
34
33
  Generate a sensible output filename based on input file or directory.
35
34
 
@@ -40,11 +39,10 @@ def guess_output_filename(input_filename: str, input_files: List[str] = None) ->
40
39
  4. For multiple files: Use the common parent directory name
41
40
 
42
41
  Args:
43
- input_filename: The input filename or pattern
44
- input_files: List of resolved input files (optional)
45
-
42
+ input_filename (str): The input filename or pattern
43
+ input_files (list[str] | None): List of resolved input files (optional)
46
44
  Returns:
47
- Generated output filename without extension
45
+ str: Generated output filename without extension
48
46
  """
49
47
  logger.debug("Guessing output filename from input: %s", input_filename)
50
48
 
@@ -0,0 +1,68 @@
1
+ import platform
2
+ from .logger import get_logger
3
+
4
+ logger = get_logger(__name__)
5
+
6
+ def handle_integration(args):
7
+ import platform
8
+ if platform.system() == 'Windows':
9
+ from .integration_windows import WindowsClassicContextMenuIntegration as ContextMenuIntegration
10
+ if args.install_integration:
11
+ success = ContextMenuIntegration.install()
12
+ if success:
13
+ logger.info("Integration installed successfully.")
14
+ return True
15
+ else:
16
+ logger.error("Integration installation failed.")
17
+ return False
18
+ elif args.uninstall_integration:
19
+ success = ContextMenuIntegration.uninstall()
20
+ if success:
21
+ logger.info("Integration uninstalled successfully.")
22
+ return True
23
+ else:
24
+ logger.error("Integration uninstallation failed.")
25
+ return False
26
+ elif platform.system() == 'Darwin':
27
+ from .integration_macos import MacOSContextMenuIntegration as ContextMenuIntegration
28
+ if args.install_integration:
29
+ success = ContextMenuIntegration.install()
30
+ if success:
31
+ logger.info("Integration installed successfully.")
32
+ return True
33
+ else:
34
+ logger.error("Integration installation failed.")
35
+ return False
36
+ elif args.uninstall_integration:
37
+ success = ContextMenuIntegration.uninstall()
38
+ if success:
39
+ logger.info("Integration uninstalled successfully.")
40
+ return True
41
+ else:
42
+ logger.error("Integration uninstallation failed.")
43
+ return False
44
+ elif platform.system() == 'Linux':
45
+ raise NotImplementedError("Context menu integration is not supported on Linux YET. But Soon™")
46
+ else:
47
+ raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
48
+
49
+ def handle_config():
50
+ """Opens the configuration file in the default text editor."""
51
+ import os
52
+ import platform
53
+ import subprocess
54
+
55
+ config_path = os.path.join(os.path.expanduser("~"), ".tonietoolbox", "config.json")
56
+
57
+ if not os.path.exists(config_path):
58
+ logger.info(f"Configuration file not found at {config_path}.")
59
+ logger.info("Creating a new configuration file. Using --install-integration will create a new config file.")
60
+ return
61
+ if platform.system() == "Windows":
62
+ os.startfile(config_path)
63
+ elif platform.system() == "Darwin":
64
+ subprocess.call(["open", config_path])
65
+ elif platform.system() == "Linux":
66
+ subprocess.call(["xdg-open", config_path])
67
+ else:
68
+ logger.error(f"Unsupported OS: {platform.system()}")