TonieToolbox 0.1.0__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,378 @@
1
+ """
2
+ Dependency management for the TonieToolbox package.
3
+
4
+ This module handles the download and management of external dependencies
5
+ required by the TonieToolbox package, such as FFmpeg and opus-tools.
6
+ """
7
+
8
+ import os
9
+ import sys
10
+ import platform
11
+ import subprocess
12
+ import shutil
13
+ import zipfile
14
+ import tarfile
15
+ import urllib.request
16
+ from pathlib import Path
17
+
18
+ from .logger import get_logger
19
+ logger = get_logger('dependency_manager')
20
+
21
+ DEPENDENCIES = {
22
+ 'ffmpeg': {
23
+ 'windows': {
24
+ 'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-win64-gpl-shared.zip',
25
+ 'bin_path': 'bin/ffmpeg.exe',
26
+ 'extract_dir': 'ffmpeg'
27
+ },
28
+ 'linux': {
29
+ 'url': 'https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-linux64-gpl-shared.tar.xz',
30
+ 'bin_path': 'ffmpeg',
31
+ 'extract_dir': 'ffmpeg'
32
+ },
33
+ 'darwin': {
34
+ 'url': 'https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip',
35
+ 'bin_path': 'ffmpeg',
36
+ 'extract_dir': 'ffmpeg'
37
+ }
38
+ },
39
+ 'opusenc': {
40
+ 'windows': {
41
+ 'url': 'https://archive.mozilla.org/pub/opus/win32/opus-tools-0.2-opus-1.3.zip',
42
+ 'bin_path': 'opusenc.exe',
43
+ 'extract_dir': 'opusenc'
44
+ },
45
+ 'linux': {
46
+ 'package': 'opus-tools'
47
+ },
48
+ 'darwin': {
49
+ 'package': 'opus-tools'
50
+ }
51
+ }
52
+ }
53
+
54
+ def get_system():
55
+ """Get the current operating system."""
56
+ system = platform.system().lower()
57
+ logger.debug("Detected operating system: %s", system)
58
+ return system
59
+
60
+ def get_user_data_dir():
61
+ """Get the user data directory for storing downloaded dependencies."""
62
+ system = get_system()
63
+
64
+ if system == 'windows':
65
+ base_dir = os.environ.get('APPDATA', os.path.expanduser('~'))
66
+ elif system == 'darwin':
67
+ base_dir = os.path.expanduser('~/Library/Application Support')
68
+ else: # linux or other unix-like
69
+ base_dir = os.environ.get('XDG_DATA_HOME', os.path.expanduser('~/.local/share'))
70
+
71
+ app_dir = os.path.join(base_dir, 'TonieToolbox')
72
+ logger.debug("Using application data directory: %s", app_dir)
73
+
74
+ os.makedirs(app_dir, exist_ok=True)
75
+ return app_dir
76
+
77
+ def download_file(url, destination):
78
+ """
79
+ Download a file from a URL to the specified destination.
80
+
81
+ Args:
82
+ url (str): The URL of the file to download
83
+ destination (str): The path to save the file to
84
+
85
+ Returns:
86
+ bool: True if download was successful, False otherwise
87
+ """
88
+ try:
89
+ logger.info("Downloading %s to %s", url, destination)
90
+ headers = {'User-Agent': 'TonieToolbox-dependency-downloader/1.0'}
91
+ req = urllib.request.Request(url, headers=headers)
92
+
93
+ with urllib.request.urlopen(req) as response, open(destination, 'wb') as out_file:
94
+ file_size = int(response.info().get('Content-Length', 0))
95
+ downloaded = 0
96
+ block_size = 8192
97
+
98
+ logger.debug("File size: %d bytes", file_size)
99
+
100
+ while True:
101
+ buffer = response.read(block_size)
102
+ if not buffer:
103
+ break
104
+
105
+ downloaded += len(buffer)
106
+ out_file.write(buffer)
107
+
108
+ if file_size > 0:
109
+ percent = downloaded * 100 / file_size
110
+ logger.debug("Download progress: %.1f%%", percent)
111
+
112
+ logger.info("Download completed successfully")
113
+ return True
114
+ except Exception as e:
115
+ logger.error("Failed to download %s: %s", url, e)
116
+ return False
117
+
118
+ def extract_archive(archive_path, extract_dir):
119
+ """
120
+ Extract an archive file to the specified directory.
121
+
122
+ Args:
123
+ archive_path (str): Path to the archive file
124
+ extract_dir (str): Directory to extract to
125
+
126
+ Returns:
127
+ bool: True if extraction was successful, False otherwise
128
+ """
129
+ try:
130
+ logger.info("Extracting %s to %s", archive_path, extract_dir)
131
+ os.makedirs(extract_dir, exist_ok=True)
132
+
133
+ if archive_path.endswith('.zip'):
134
+ logger.debug("Extracting ZIP archive")
135
+ with zipfile.ZipFile(archive_path, 'r') as zip_ref:
136
+ zip_ref.extractall(extract_dir)
137
+ logger.trace("Extracted files: %s", zip_ref.namelist())
138
+ elif archive_path.endswith(('.tar.gz', '.tgz')):
139
+ logger.debug("Extracting TAR.GZ archive")
140
+ with tarfile.open(archive_path, 'r:gz') as tar_ref:
141
+ tar_ref.extractall(extract_dir)
142
+ logger.trace("Extracted files: %s", tar_ref.getnames())
143
+ elif archive_path.endswith(('.tar.xz', '.txz')):
144
+ logger.debug("Extracting TAR.XZ archive")
145
+ with tarfile.open(archive_path, 'r:xz') as tar_ref:
146
+ tar_ref.extractall(extract_dir)
147
+ logger.trace("Extracted files: %s", tar_ref.getnames())
148
+ elif archive_path.endswith('.tar'):
149
+ logger.debug("Extracting TAR archive")
150
+ with tarfile.open(archive_path, 'r') as tar_ref:
151
+ tar_ref.extractall(extract_dir)
152
+ logger.trace("Extracted files: %s", tar_ref.getnames())
153
+ else:
154
+ logger.error("Unsupported archive format: %s", archive_path)
155
+ return False
156
+
157
+ logger.info("Archive extracted successfully")
158
+ return True
159
+ except Exception as e:
160
+ logger.error("Failed to extract %s: %s", archive_path, e)
161
+ return False
162
+
163
+ def find_binary_in_extracted_dir(extract_dir, binary_path):
164
+ """
165
+ Find a binary file in the extracted directory structure.
166
+
167
+ Args:
168
+ extract_dir (str): Directory where the archive was extracted
169
+ binary_path (str): Path or name of the binary to find
170
+
171
+ Returns:
172
+ str: Full path to the binary if found, None otherwise
173
+ """
174
+ logger.debug("Looking for binary %s in %s", binary_path, extract_dir)
175
+
176
+ direct_path = os.path.join(extract_dir, binary_path)
177
+ if os.path.exists(direct_path):
178
+ logger.debug("Found binary at direct path: %s", direct_path)
179
+ return direct_path
180
+
181
+ logger.debug("Searching for binary in directory tree")
182
+ for root, _, files in os.walk(extract_dir):
183
+ for f in files:
184
+ if f == os.path.basename(binary_path) or f == binary_path:
185
+ full_path = os.path.join(root, f)
186
+ logger.debug("Found binary at: %s", full_path)
187
+ return full_path
188
+
189
+ logger.warning("Binary %s not found in %s", binary_path, extract_dir)
190
+ return None
191
+
192
+ def check_binary_in_path(binary_name):
193
+ """
194
+ Check if a binary is available in PATH.
195
+
196
+ Args:
197
+ binary_name (str): Name of the binary to check
198
+
199
+ Returns:
200
+ str: Path to the binary if found, None otherwise
201
+ """
202
+ logger.debug("Checking if %s is available in PATH", binary_name)
203
+ try:
204
+ path = shutil.which(binary_name)
205
+ if path:
206
+ logger.debug("Found %s at %s, verifying it works", binary_name, path)
207
+
208
+ if binary_name == 'opusenc':
209
+ # Try with --version flag first
210
+ cmd = [path, '--version']
211
+ result = subprocess.run(cmd,
212
+ stdout=subprocess.PIPE,
213
+ stderr=subprocess.PIPE,
214
+ timeout=5)
215
+
216
+ # If --version fails, try without arguments (opusenc shows help/version when run without args)
217
+ if result.returncode != 0:
218
+ logger.debug("opusenc --version failed, trying without arguments")
219
+ result = subprocess.run([path],
220
+ stdout=subprocess.PIPE,
221
+ stderr=subprocess.PIPE,
222
+ timeout=5)
223
+ else:
224
+ # For other binaries like ffmpeg
225
+ cmd = [path, '-version']
226
+ result = subprocess.run(cmd,
227
+ stdout=subprocess.PIPE,
228
+ stderr=subprocess.PIPE,
229
+ timeout=5)
230
+
231
+ if result.returncode == 0:
232
+ logger.debug("%s is available and working", binary_name)
233
+ return path
234
+ else:
235
+ logger.warning("%s found but returned error code %d", binary_name, result.returncode)
236
+ else:
237
+ logger.debug("%s not found in PATH", binary_name)
238
+ except Exception as e:
239
+ logger.warning("Error checking %s: %s", binary_name, e)
240
+
241
+ return None
242
+
243
+ def install_package(package_name):
244
+ """
245
+ Attempt to install a package using the system's package manager.
246
+
247
+ Args:
248
+ package_name (str): Name of the package to install
249
+
250
+ Returns:
251
+ bool: True if installation was successful, False otherwise
252
+ """
253
+ system = get_system()
254
+ logger.info("Attempting to install %s on %s", package_name, system)
255
+
256
+ try:
257
+ if system == 'linux':
258
+ # Try apt-get (Debian/Ubuntu)
259
+ if shutil.which('apt-get'):
260
+ logger.info("Installing %s using apt-get", package_name)
261
+ subprocess.run(['sudo', 'apt-get', 'update'], check=True)
262
+ subprocess.run(['sudo', 'apt-get', 'install', '-y', package_name], check=True)
263
+ return True
264
+ # Try yum (CentOS/RHEL)
265
+ elif shutil.which('yum'):
266
+ logger.info("Installing %s using yum", package_name)
267
+ subprocess.run(['sudo', 'yum', 'install', '-y', package_name], check=True)
268
+ return True
269
+
270
+ elif system == 'darwin':
271
+ # Try Homebrew
272
+ if shutil.which('brew'):
273
+ logger.info("Installing %s using homebrew", package_name)
274
+ subprocess.run(['brew', 'install', package_name], check=True)
275
+ return True
276
+
277
+ logger.warning("Could not automatically install %s. Please install it manually.", package_name)
278
+ return False
279
+ except subprocess.CalledProcessError as e:
280
+ logger.error("Failed to install %s: %s", package_name, e)
281
+ return False
282
+
283
+ def ensure_dependency(dependency_name):
284
+ """
285
+ Ensure that a dependency is available, downloading it if necessary.
286
+
287
+ Args:
288
+ dependency_name (str): Name of the dependency ('ffmpeg' or 'opusenc')
289
+
290
+ Returns:
291
+ str: Path to the binary if available, None otherwise
292
+ """
293
+ logger.info("Ensuring dependency: %s", dependency_name)
294
+ system = get_system()
295
+
296
+ if system not in ['windows', 'linux', 'darwin']:
297
+ logger.error("Unsupported operating system: %s", system)
298
+ return None
299
+
300
+ if dependency_name not in DEPENDENCIES:
301
+ logger.error("Unknown dependency: %s", dependency_name)
302
+ return None
303
+
304
+ # First check if it's already in PATH
305
+ bin_name = dependency_name if dependency_name != 'opusenc' else 'opusenc'
306
+ path_binary = check_binary_in_path(bin_name)
307
+ if path_binary:
308
+ logger.info("Found %s in PATH: %s", dependency_name, path_binary)
309
+ return path_binary
310
+
311
+ # If not in PATH, check if we should install via package manager
312
+ if 'package' in DEPENDENCIES[dependency_name].get(system, {}):
313
+ package_name = DEPENDENCIES[dependency_name][system]['package']
314
+ logger.info("%s not found. Attempting to install %s package...", dependency_name, package_name)
315
+ if install_package(package_name):
316
+ path_binary = check_binary_in_path(bin_name)
317
+ if path_binary:
318
+ logger.info("Successfully installed %s: %s", dependency_name, path_binary)
319
+ return path_binary
320
+
321
+ # If not installable via package manager or installation failed, try downloading
322
+ if 'url' not in DEPENDENCIES[dependency_name].get(system, {}):
323
+ logger.error("Cannot download %s for %s", dependency_name, system)
324
+ return None
325
+
326
+ # Set up paths
327
+ user_data_dir = get_user_data_dir()
328
+ dependency_info = DEPENDENCIES[dependency_name][system]
329
+ download_url = dependency_info['url']
330
+ extract_dir_name = dependency_info['extract_dir']
331
+ binary_path = dependency_info['bin_path']
332
+
333
+ extract_dir = os.path.join(user_data_dir, extract_dir_name)
334
+ logger.debug("Using extract directory: %s", extract_dir)
335
+ os.makedirs(extract_dir, exist_ok=True)
336
+
337
+ # Check if we already downloaded and extracted it
338
+ existing_binary = find_binary_in_extracted_dir(extract_dir, binary_path)
339
+ if existing_binary and os.path.exists(existing_binary):
340
+ logger.info("Using existing %s: %s", dependency_name, existing_binary)
341
+ return existing_binary
342
+
343
+ # Download and extract
344
+ archive_ext = '.zip' if download_url.endswith('zip') else '.tar.xz'
345
+ archive_path = os.path.join(user_data_dir, f"{dependency_name}{archive_ext}")
346
+ logger.debug("Using archive path: %s", archive_path)
347
+
348
+ if download_file(download_url, archive_path):
349
+ if extract_archive(archive_path, extract_dir):
350
+ binary = find_binary_in_extracted_dir(extract_dir, binary_path)
351
+ if binary:
352
+ # Make sure it's executable on Unix-like systems
353
+ if system in ['linux', 'darwin']:
354
+ logger.debug("Setting executable permissions on %s", binary)
355
+ os.chmod(binary, 0o755)
356
+ logger.info("Successfully set up %s: %s", dependency_name, binary)
357
+ return binary
358
+
359
+ logger.error("Failed to set up %s", dependency_name)
360
+ return None
361
+
362
+ def get_ffmpeg_binary():
363
+ """
364
+ Get the path to the FFmpeg binary, downloading it if necessary.
365
+
366
+ Returns:
367
+ str: Path to the FFmpeg binary if available, None otherwise
368
+ """
369
+ return ensure_dependency('ffmpeg')
370
+
371
+ def get_opus_binary():
372
+ """
373
+ Get the path to the opusenc binary, downloading it if necessary.
374
+
375
+ Returns:
376
+ str: Path to the opusenc binary if available, None otherwise
377
+ """
378
+ return ensure_dependency('opusenc')
@@ -0,0 +1,94 @@
1
+ """
2
+ Module for generating intelligent output filenames for TonieToolbox.
3
+ """
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import List, Optional
9
+ from .logger import get_logger
10
+
11
+ logger = get_logger('filename_generator')
12
+
13
+ def sanitize_filename(filename: str) -> str:
14
+ """
15
+ Sanitize a filename by removing invalid characters and trimming.
16
+
17
+ Args:
18
+ filename: The filename to sanitize
19
+
20
+ Returns:
21
+ A sanitized filename
22
+ """
23
+ # Remove invalid characters for filenames
24
+ sanitized = re.sub(r'[<>:"/\\|?*]', '_', filename)
25
+ # Remove leading/trailing whitespace and dots
26
+ sanitized = sanitized.strip('. \t')
27
+ # Avoid empty filenames
28
+ if not sanitized:
29
+ return "tonie"
30
+ return sanitized
31
+
32
+ def guess_output_filename(input_filename: str, input_files: List[str] = None) -> str:
33
+ """
34
+ Generate a sensible output filename based on input file or directory.
35
+
36
+ Logic:
37
+ 1. For .lst files: Use the lst filename without extension
38
+ 2. For directories: Use the directory name
39
+ 3. For single files: Use the filename without extension
40
+ 4. For multiple files: Use the common parent directory name
41
+
42
+ Args:
43
+ input_filename: The input filename or pattern
44
+ input_files: List of resolved input files (optional)
45
+
46
+ Returns:
47
+ Generated output filename without extension
48
+ """
49
+ logger.debug("Guessing output filename from input: %s", input_filename)
50
+
51
+ # Handle .lst files
52
+ if input_filename.lower().endswith('.lst'):
53
+ base = os.path.basename(input_filename)
54
+ name = os.path.splitext(base)[0]
55
+ logger.debug("Using .lst file name: %s", name)
56
+ return sanitize_filename(name)
57
+
58
+ # Handle directory pattern
59
+ if input_filename.endswith('/*') or input_filename.endswith('\\*'):
60
+ dir_path = input_filename[:-2] # Remove the /* or \* at the end
61
+ dir_name = os.path.basename(os.path.normpath(dir_path))
62
+ logger.debug("Using directory name: %s", dir_name)
63
+ return sanitize_filename(dir_name)
64
+
65
+ # Handle directory
66
+ if os.path.isdir(input_filename):
67
+ dir_name = os.path.basename(os.path.normpath(input_filename))
68
+ logger.debug("Using directory name: %s", dir_name)
69
+ return sanitize_filename(dir_name)
70
+
71
+ # Handle single file
72
+ if not input_files or len(input_files) == 1:
73
+ file_path = input_files[0] if input_files else input_filename
74
+ base = os.path.basename(file_path)
75
+ name = os.path.splitext(base)[0]
76
+ logger.debug("Using single file name: %s", name)
77
+ return sanitize_filename(name)
78
+
79
+ # Handle multiple files - try to find common parent directory
80
+ try:
81
+ # Find the common parent directory of all files
82
+ common_path = os.path.commonpath([os.path.abspath(f) for f in input_files])
83
+ dir_name = os.path.basename(common_path)
84
+
85
+ # If the common path is root or very short, use parent of first file instead
86
+ if len(dir_name) <= 1 or len(common_path) < 4:
87
+ dir_name = os.path.basename(os.path.dirname(os.path.abspath(input_files[0])))
88
+
89
+ logger.debug("Using common parent directory: %s", dir_name)
90
+ return sanitize_filename(dir_name)
91
+ except ValueError:
92
+ # Files might be on different drives
93
+ logger.debug("Could not determine common path, using generic name")
94
+ return "tonie_collection"
TonieToolbox/logger.py ADDED
@@ -0,0 +1,57 @@
1
+ """
2
+ Logging configuration for the TonieToolbox package.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ import sys
8
+
9
+ # Define log levels and their names
10
+ TRACE = 5 # Custom level for ultra-verbose debugging
11
+ logging.addLevelName(TRACE, 'TRACE')
12
+
13
+ # Create a method for the TRACE level
14
+ def trace(self, message, *args, **kwargs):
15
+ """Log a message with TRACE level (more detailed than DEBUG)"""
16
+ if self.isEnabledFor(TRACE):
17
+ self.log(TRACE, message, *args, **kwargs)
18
+
19
+ # Add trace method to the Logger class
20
+ logging.Logger.trace = trace
21
+
22
+ def setup_logging(level=logging.INFO):
23
+ """
24
+ Set up logging configuration for the entire application.
25
+
26
+ Args:
27
+ level: Logging level (default: logging.INFO)
28
+
29
+ Returns:
30
+ logging.Logger: Root logger instance
31
+ """
32
+ # Configure root logger
33
+ logging.basicConfig(
34
+ level=level,
35
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
36
+ datefmt='%Y-%m-%d %H:%M:%S'
37
+ )
38
+
39
+ # Get the root logger
40
+ root_logger = logging.getLogger('TonieToolbox')
41
+ root_logger.setLevel(level)
42
+
43
+ return root_logger
44
+
45
+ def get_logger(name):
46
+ """
47
+ Get a logger with the specified name.
48
+
49
+ Args:
50
+ name: Logger name, typically the module name
51
+
52
+ Returns:
53
+ logging.Logger: Logger instance
54
+ """
55
+ # Get logger with proper hierarchical naming
56
+ logger = logging.getLogger(f'TonieToolbox.{name}')
57
+ return logger