TonieToolbox 0.6.1__tar.gz → 0.6.5__tar.gz
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-0.6.1/TonieToolbox.egg-info → tonietoolbox-0.6.5}/PKG-INFO +6 -9
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/__init__.py +1 -1
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/__main__.py +218 -21
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/audio_conversion.py +77 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/dependency_manager.py +60 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/integration.py +37 -1
- tonietoolbox-0.6.5/TonieToolbox/integration_kde.py +677 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/integration_windows.py +4 -2
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/media_tags.py +95 -0
- tonietoolbox-0.6.5/TonieToolbox/player.py +638 -0
- tonietoolbox-0.6.5/TonieToolbox/player_gui.py +1212 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/recursive_processor.py +29 -1
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/teddycloud.py +79 -4
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/tonie_analysis.py +235 -1
- tonietoolbox-0.6.5/TonieToolbox/tonie_header_pb2.py +38 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5/TonieToolbox.egg-info}/PKG-INFO +6 -9
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox.egg-info/SOURCES.txt +3 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox.egg-info/requires.txt +2 -2
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/pyproject.toml +6 -9
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/setup.py +7 -10
- tonietoolbox-0.6.1/TonieToolbox/tonie_header_pb2.py +0 -99
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/LICENSE.md +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/MANIFEST.in +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/README.md +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/artwork.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/constants.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/filename_generator.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/integration_macos.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/integration_ubuntu.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/logger.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/ogg_page.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/opus_packet.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/tags.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/tonie_file.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/tonie_header.proto +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/tonies_json.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox/version_handler.py +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox.egg-info/dependency_links.txt +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox.egg-info/entry_points.txt +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/TonieToolbox.egg-info/top_level.txt +0 -0
- {tonietoolbox-0.6.1 → tonietoolbox-0.6.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: TonieToolbox
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.5
|
|
4
4
|
Summary: Convert audio files to Toniebox compatible format (.TAF) and interact with TeddyCloud.
|
|
5
5
|
Home-page: https://github.com/Quentendo64/TonieToolbox
|
|
6
6
|
Author: Quentendo64
|
|
@@ -8,19 +8,16 @@ Author-email: Quentendo64 <quentin@wohlfeil.at>
|
|
|
8
8
|
License-Expression: GPL-3.0-or-later
|
|
9
9
|
Project-URL: Homepage, https://github.com/Quentendo64/TonieToolbox
|
|
10
10
|
Project-URL: Bug Tracker, https://github.com/Quentendo64/TonieToolbox/issues
|
|
11
|
-
Classifier: Programming Language :: Python :: 3
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
14
|
Classifier: Operating System :: OS Independent
|
|
18
15
|
Classifier: Topic :: Multimedia :: Sound/Audio :: Conversion
|
|
19
16
|
Requires-Python: >=3.6
|
|
20
17
|
Description-Content-Type: text/markdown
|
|
21
18
|
License-File: LICENSE.md
|
|
22
|
-
Requires-Dist: protobuf<=
|
|
23
|
-
Requires-Dist: requests>=2.32.
|
|
19
|
+
Requires-Dist: protobuf<=6.33.0
|
|
20
|
+
Requires-Dist: requests>=2.32.5
|
|
24
21
|
Requires-Dist: mutagen>=1.47.0
|
|
25
22
|
Requires-Dist: packaging>=25.0
|
|
26
23
|
Requires-Dist: tqdm>=4.67.1
|
|
@@ -10,19 +10,23 @@ import logging
|
|
|
10
10
|
from . import __version__
|
|
11
11
|
from .audio_conversion import get_input_files, append_to_filename
|
|
12
12
|
from .tonie_file import create_tonie_file
|
|
13
|
-
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
|
|
13
|
+
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files, extract_to_mp3_files, extract_full_audio_to_mp3
|
|
14
|
+
from .player import interactive_player
|
|
15
|
+
from .player_gui import gui_player
|
|
14
16
|
from .dependency_manager import get_ffmpeg_binary, get_opus_binary, ensure_dependency
|
|
15
17
|
from .logger import TRACE, setup_logging, get_logger
|
|
16
18
|
from .filename_generator import guess_output_filename, apply_template_to_path,ensure_directory_exists
|
|
17
19
|
from .version_handler import check_for_updates, clear_version_cache
|
|
18
|
-
from .recursive_processor import process_recursive_folders
|
|
19
|
-
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags
|
|
20
|
+
from .recursive_processor import process_recursive_folders, find_audio_folders, get_all_audio_files_recursive
|
|
21
|
+
from .media_tags import is_available as is_media_tags_available, ensure_mutagen, extract_album_info, format_metadata_filename, get_file_tags, get_all_file_tags
|
|
20
22
|
from .teddycloud import TeddyCloudClient
|
|
21
23
|
from .tags import get_tags
|
|
22
24
|
from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
|
|
23
25
|
from .artwork import upload_artwork
|
|
24
26
|
from .integration import handle_integration, handle_config
|
|
25
27
|
|
|
28
|
+
|
|
29
|
+
|
|
26
30
|
def main():
|
|
27
31
|
"""Entry point for the TonieToolbox application."""
|
|
28
32
|
parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
|
|
@@ -34,6 +38,8 @@ def main():
|
|
|
34
38
|
help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
|
|
35
39
|
teddycloud_group.add_argument('--include-artwork', action='store_true',
|
|
36
40
|
help='Upload cover artwork image alongside the Tonie file when using --upload')
|
|
41
|
+
teddycloud_group.add_argument('--assign-to-tag', action='store_true',
|
|
42
|
+
help='Assign the uploaded file to a specific tag ID')
|
|
37
43
|
teddycloud_group.add_argument('--get-tags', action='store', metavar='URL',
|
|
38
44
|
help='Get available tags from TeddyCloud instance')
|
|
39
45
|
teddycloud_group.add_argument('--ignore-ssl-verify', action='store_true',
|
|
@@ -74,17 +80,23 @@ def main():
|
|
|
74
80
|
# ------------- Parser - Librarys -------------
|
|
75
81
|
parser.add_argument('-f', '--ffmpeg', help='specify location of ffmpeg', default=None)
|
|
76
82
|
parser.add_argument('-o', '--opusenc', help='specify location of opusenc', default=None)
|
|
77
|
-
parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
|
|
83
|
+
parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps for Opus & MP3 Conversion (default: 96)', default=96)
|
|
78
84
|
parser.add_argument('-c', '--cbr', action='store_true', help='encode in cbr mode')
|
|
79
85
|
parser.add_argument('--auto-download', action='store_true',
|
|
80
86
|
help='automatically download ffmpeg and opusenc if not found')
|
|
81
|
-
# ------------- Parser - TAF -------------
|
|
87
|
+
# ------------- Parser - TAF -------------
|
|
82
88
|
parser.add_argument('-a', '--append-tonie-tag', metavar='TAG', action='store',
|
|
83
89
|
help='append [TAG] to filename (must be an 8-character hex value)')
|
|
84
90
|
parser.add_argument('-n', '--no-tonie-header', action='store_true', help='do not write Tonie header')
|
|
85
91
|
parser.add_argument('-i', '--info', action='store_true', help='Check and display info about Tonie file')
|
|
92
|
+
parser.add_argument('-p', '--play', action='store_true', help='Play TAF audio file with interactive controls')
|
|
93
|
+
parser.add_argument('--play-ui', action='store_true', help='Play TAF audio file with minimal GUI interface (requires tkinter)')
|
|
86
94
|
parser.add_argument('-s', '--split', action='store_true', help='Split Tonie file into opus tracks')
|
|
87
95
|
parser.add_argument('-r', '--recursive', action='store_true', help='Process folders recursively')
|
|
96
|
+
parser.add_argument('--files-to-taf', action='store_true',
|
|
97
|
+
help='Convert each audio file in a directory to individual .taf files')
|
|
98
|
+
parser.add_argument('--convert-to-separate-mp3', action='store_true', help='Convert Tonie file to individual MP3 tracks')
|
|
99
|
+
parser.add_argument('--convert-to-single-mp3', action='store_true', help='Convert Tonie file to a single MP3 file')
|
|
88
100
|
parser.add_argument('-O', '--output-to-source', action='store_true',
|
|
89
101
|
help='Save output files in the source directory instead of output directory')
|
|
90
102
|
parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
|
|
@@ -134,9 +146,7 @@ def main():
|
|
|
134
146
|
log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
|
|
135
147
|
log_group.add_argument('--log-file', action='store_true', default=False,
|
|
136
148
|
help='Save logs to a timestamped file in .tonietoolbox folder')
|
|
137
|
-
args = parser.parse_args()
|
|
138
|
-
|
|
139
|
-
# ------------- Parser - Source Input -------------
|
|
149
|
+
args = parser.parse_args() # ------------- Parser - Source Input -------------
|
|
140
150
|
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 or args.auto_download):
|
|
141
151
|
parser.error("the following arguments are required: SOURCE")
|
|
142
152
|
|
|
@@ -178,17 +188,25 @@ def main():
|
|
|
178
188
|
logger.info("Update available but user chose to continue without updating.")
|
|
179
189
|
|
|
180
190
|
# ------------- Autodownload & Dependency Checks -------------
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
else:
|
|
190
|
-
logger.error("Failed to download ffmpeg or opusenc. Please install them manually.")
|
|
191
|
+
# ------------- Librarys / Prereqs -------------
|
|
192
|
+
logger.debug("Checking for external dependencies")
|
|
193
|
+
ffmpeg_binary = args.ffmpeg
|
|
194
|
+
if ffmpeg_binary is None:
|
|
195
|
+
logger.debug("No FFmpeg specified, attempting to locate binary (auto_download=%s)", args.auto_download)
|
|
196
|
+
ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
|
|
197
|
+
if ffmpeg_binary is None:
|
|
198
|
+
logger.error("Could not find FFmpeg. Please install FFmpeg or specify its location using --ffmpeg or use --auto-download")
|
|
191
199
|
sys.exit(1)
|
|
200
|
+
logger.debug("Using FFmpeg binary: %s", ffmpeg_binary)
|
|
201
|
+
|
|
202
|
+
opus_binary = args.opusenc
|
|
203
|
+
if opus_binary is None:
|
|
204
|
+
logger.debug("No opusenc specified, attempting to locate binary (auto_download=%s)", args.auto_download)
|
|
205
|
+
opus_binary = get_opus_binary(args.auto_download)
|
|
206
|
+
if opus_binary is None:
|
|
207
|
+
logger.error("Could not find opusenc. Please install opus-tools or specify its location using --opusenc or use --auto-download")
|
|
208
|
+
sys.exit(1)
|
|
209
|
+
logger.debug("Using opusenc binary: %s", opus_binary)
|
|
192
210
|
|
|
193
211
|
# ------------- Context Menu Integration -------------
|
|
194
212
|
if args.install_integration or args.uninstall_integration:
|
|
@@ -207,10 +225,158 @@ def main():
|
|
|
207
225
|
else:
|
|
208
226
|
logger.error("FFmpeg and opusenc are required for context menu integration")
|
|
209
227
|
sys.exit(1)
|
|
228
|
+
|
|
210
229
|
if args.config_integration:
|
|
211
230
|
logger.debug("Opening configuration file for editing")
|
|
212
231
|
handle_config()
|
|
213
232
|
sys.exit(0)
|
|
233
|
+
# ------------- Files to TAF Processing -------------
|
|
234
|
+
if args.files_to_taf:
|
|
235
|
+
if args.recursive:
|
|
236
|
+
logger.info("Processing individual files to separate TAF files recursively: %s", args.input_filename)
|
|
237
|
+
else:
|
|
238
|
+
logger.info("Processing individual files to separate TAF files: %s", args.input_filename)
|
|
239
|
+
|
|
240
|
+
if not os.path.isdir(args.input_filename):
|
|
241
|
+
logger.error("--files-to-taf requires a directory as input")
|
|
242
|
+
sys.exit(1)
|
|
243
|
+
|
|
244
|
+
# Use recursive file discovery if --recursive flag is also specified
|
|
245
|
+
if args.recursive:
|
|
246
|
+
audio_files = get_all_audio_files_recursive(args.input_filename)
|
|
247
|
+
else:
|
|
248
|
+
audio_files = get_input_files(args.input_filename)
|
|
249
|
+
|
|
250
|
+
if not audio_files:
|
|
251
|
+
search_type = "recursively" if args.recursive else "in directory"
|
|
252
|
+
logger.error("No audio files found %s: %s", search_type, args.input_filename)
|
|
253
|
+
sys.exit(1)
|
|
254
|
+
|
|
255
|
+
logger.info("Found %d audio files to convert", len(audio_files))
|
|
256
|
+
|
|
257
|
+
output_dir = args.input_filename if args.output_to_source else './output'
|
|
258
|
+
|
|
259
|
+
if not args.output_to_source and not os.path.exists(output_dir):
|
|
260
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
261
|
+
logger.debug("Created output directory: %s", output_dir)
|
|
262
|
+
|
|
263
|
+
created_files = []
|
|
264
|
+
for file_index, audio_file in enumerate(audio_files):
|
|
265
|
+
# Generate output filename based on the original file
|
|
266
|
+
base_name = os.path.splitext(os.path.basename(audio_file))[0]
|
|
267
|
+
|
|
268
|
+
# Apply media tag naming if requested
|
|
269
|
+
if args.use_media_tags:
|
|
270
|
+
tags = get_file_tags(audio_file)
|
|
271
|
+
if tags:
|
|
272
|
+
template = args.name_template or "{artist} - {title}"
|
|
273
|
+
new_name = format_metadata_filename(tags, template)
|
|
274
|
+
if new_name:
|
|
275
|
+
logger.debug("Using media tags for file naming: %s -> %s", base_name, new_name)
|
|
276
|
+
base_name = new_name
|
|
277
|
+
|
|
278
|
+
# Apply tonie tag if specified
|
|
279
|
+
if args.append_tonie_tag:
|
|
280
|
+
hex_tag = args.append_tonie_tag
|
|
281
|
+
if not all(c in '0123456789abcdefABCDEF' for c in hex_tag) or len(hex_tag) != 8:
|
|
282
|
+
logger.error("TAG must be an 8-character hexadecimal value")
|
|
283
|
+
sys.exit(1)
|
|
284
|
+
base_name = append_to_filename(base_name, hex_tag)
|
|
285
|
+
|
|
286
|
+
output_filename = os.path.join(output_dir, f"{base_name}.taf")
|
|
287
|
+
|
|
288
|
+
# Check if file already exists
|
|
289
|
+
skip_creation = False
|
|
290
|
+
if os.path.exists(output_filename):
|
|
291
|
+
logger.warning("Output file already exists: %s", output_filename)
|
|
292
|
+
valid_taf = check_tonie_file_cli(output_filename)
|
|
293
|
+
|
|
294
|
+
if valid_taf and not args.force_creation:
|
|
295
|
+
logger.warning("Valid Tonie file exists, skipping: %s", output_filename)
|
|
296
|
+
skip_creation = True
|
|
297
|
+
else:
|
|
298
|
+
logger.info("Output file exists but is not valid, proceeding to create new one")
|
|
299
|
+
|
|
300
|
+
logger.info("[%d/%d] Converting: %s -> %s",
|
|
301
|
+
file_index + 1, len(audio_files), os.path.basename(audio_file), os.path.basename(output_filename))
|
|
302
|
+
|
|
303
|
+
if not skip_creation:
|
|
304
|
+
try:
|
|
305
|
+
create_tonie_file(output_filename, [audio_file], args.no_tonie_header, args.user_timestamp,
|
|
306
|
+
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
|
307
|
+
args.auto_download, not args.use_legacy_tags,
|
|
308
|
+
no_mono_conversion=args.no_mono_conversion)
|
|
309
|
+
logger.info("Successfully created: %s", output_filename)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
logger.error("Failed to create %s: %s", output_filename, str(e))
|
|
312
|
+
continue
|
|
313
|
+
|
|
314
|
+
created_files.append(output_filename)
|
|
315
|
+
|
|
316
|
+
# Handle upload if requested
|
|
317
|
+
if args.upload:
|
|
318
|
+
upload_path = args.path
|
|
319
|
+
if upload_path and '{' in upload_path and args.use_media_tags:
|
|
320
|
+
metadata = get_file_tags(audio_file)
|
|
321
|
+
if metadata:
|
|
322
|
+
formatted_path = apply_template_to_path(upload_path, metadata)
|
|
323
|
+
if formatted_path:
|
|
324
|
+
logger.info("Using dynamic upload path from template: %s", formatted_path)
|
|
325
|
+
upload_path = formatted_path
|
|
326
|
+
else:
|
|
327
|
+
logger.warning("Could not apply all tags to path template '%s'. Using as-is.", upload_path)
|
|
328
|
+
|
|
329
|
+
# Create directories recursively if path is provided
|
|
330
|
+
if upload_path:
|
|
331
|
+
logger.debug("Creating directory structure on server: %s", upload_path)
|
|
332
|
+
try:
|
|
333
|
+
client.create_directories_recursive(
|
|
334
|
+
path=upload_path,
|
|
335
|
+
special=args.special_folder
|
|
336
|
+
)
|
|
337
|
+
logger.debug("Successfully created directory structure on server")
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.warning("Failed to create directory structure on server: %s", str(e))
|
|
340
|
+
logger.debug("Continuing with upload anyway, in case the directory already exists")
|
|
341
|
+
|
|
342
|
+
response = client.upload_file(
|
|
343
|
+
file_path=output_filename,
|
|
344
|
+
destination_path=upload_path,
|
|
345
|
+
special=args.special_folder,
|
|
346
|
+
)
|
|
347
|
+
upload_success = response.get('success', False)
|
|
348
|
+
|
|
349
|
+
if not upload_success:
|
|
350
|
+
logger.error("Failed to upload %s to TeddyCloud", output_filename)
|
|
351
|
+
else:
|
|
352
|
+
logger.info("Successfully uploaded %s to TeddyCloud", output_filename)
|
|
353
|
+
|
|
354
|
+
# Handle artwork upload
|
|
355
|
+
artwork_url = None
|
|
356
|
+
if args.include_artwork:
|
|
357
|
+
success, artwork_url = upload_artwork(client, output_filename, os.path.dirname(audio_file), [audio_file])
|
|
358
|
+
if success:
|
|
359
|
+
logger.info("Successfully uploaded artwork for %s", output_filename)
|
|
360
|
+
else:
|
|
361
|
+
logger.warning("Failed to upload artwork for %s", output_filename)
|
|
362
|
+
|
|
363
|
+
# Handle custom JSON creation
|
|
364
|
+
if args.create_custom_json:
|
|
365
|
+
json_output_dir = args.input_filename if args.output_to_source else output_dir
|
|
366
|
+
client_param = client if 'client' in locals() else None
|
|
367
|
+
if args.version_2:
|
|
368
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
|
369
|
+
success = fetch_and_update_tonies_json_v2(client_param, output_filename, [audio_file], artwork_url, json_output_dir)
|
|
370
|
+
else:
|
|
371
|
+
success = fetch_and_update_tonies_json_v1(client_param, output_filename, [audio_file], artwork_url, json_output_dir)
|
|
372
|
+
if success:
|
|
373
|
+
logger.info("Successfully updated Tonies JSON for %s", output_filename)
|
|
374
|
+
else:
|
|
375
|
+
logger.warning("Failed to update Tonies JSON for %s", output_filename)
|
|
376
|
+
|
|
377
|
+
logger.info("Files to TAF processing completed. Created %d Tonie files.", len(created_files))
|
|
378
|
+
sys.exit(0)
|
|
379
|
+
|
|
214
380
|
# ------------- Normalize Path Input -------------
|
|
215
381
|
if args.input_filename:
|
|
216
382
|
logger.debug("Original input path: %s", args.input_filename)
|
|
@@ -258,7 +424,7 @@ def main():
|
|
|
258
424
|
logger.error("No files found for pattern %s", args.input_filename)
|
|
259
425
|
sys.exit(1)
|
|
260
426
|
for file_index, file_path in enumerate(files):
|
|
261
|
-
tags =
|
|
427
|
+
tags = get_all_file_tags(file_path)
|
|
262
428
|
if tags:
|
|
263
429
|
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
|
264
430
|
print("-" * 40)
|
|
@@ -323,6 +489,21 @@ def main():
|
|
|
323
489
|
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
|
324
490
|
logger.debug("Upload response details: %s",
|
|
325
491
|
{k: v for k, v in response.items() if k != 'success'})
|
|
492
|
+
if args.assign_to_tag:
|
|
493
|
+
tag_id = input("Enter the tag ID to assign the uploaded file: eg. 'E0:04:03:50:11:AA:7E:81': ").strip()
|
|
494
|
+
fileName = os.path.basename(file_path)
|
|
495
|
+
if upload_path:
|
|
496
|
+
libPath = f"lib://{upload_path}/{fileName}"
|
|
497
|
+
else:
|
|
498
|
+
libPath = f"lib://{fileName}"
|
|
499
|
+
logger.info("Assigning uploaded file %s to tag ID: %s", fileName, tag_id)
|
|
500
|
+
logger.debug("Library path for assignment: %s", libPath)
|
|
501
|
+
success = client.assign_tag_path(libPath, tag_id)
|
|
502
|
+
if success:
|
|
503
|
+
logger.info("Successfully assigned tag %s to %s", tag_id, fileName)
|
|
504
|
+
else:
|
|
505
|
+
logger.warning("Failed to assign tag %s to %s", tag_id, fileName)
|
|
506
|
+
|
|
326
507
|
artwork_url = None
|
|
327
508
|
if args.include_artwork and file_path.lower().endswith('.taf'):
|
|
328
509
|
source_dir = os.path.dirname(file_path)
|
|
@@ -500,6 +681,22 @@ def main():
|
|
|
500
681
|
logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
|
|
501
682
|
result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
|
|
502
683
|
sys.exit(0 if result else 1)
|
|
684
|
+
elif args.play:
|
|
685
|
+
logger.info("Playing Tonie file: %s", args.input_filename)
|
|
686
|
+
interactive_player(args.input_filename)
|
|
687
|
+
sys.exit(0)
|
|
688
|
+
elif args.play_ui:
|
|
689
|
+
logger.info("Starting GUI player for Tonie file: %s", args.input_filename)
|
|
690
|
+
gui_player(args.input_filename)
|
|
691
|
+
sys.exit(0)
|
|
692
|
+
elif args.convert_to_separate_mp3:
|
|
693
|
+
logger.info("Converting Tonie file to separate MP3 tracks: %s", args.input_filename)
|
|
694
|
+
extract_to_mp3_files(args.input_filename, args.output_filename, args.bitrate)
|
|
695
|
+
sys.exit(0)
|
|
696
|
+
elif args.convert_to_single_mp3:
|
|
697
|
+
logger.info("Converting Tonie file to single MP3: %s", args.input_filename)
|
|
698
|
+
extract_full_audio_to_mp3(args.input_filename, args.output_filename, args.bitrate)
|
|
699
|
+
sys.exit(0)
|
|
503
700
|
|
|
504
701
|
files = get_input_files(args.input_filename)
|
|
505
702
|
logger.debug("Found %d files to process", len(files))
|
|
@@ -600,7 +797,7 @@ def main():
|
|
|
600
797
|
out_filename = formatted_path
|
|
601
798
|
logger.info("Using template path for output: %s", out_filename)
|
|
602
799
|
else:
|
|
603
|
-
logger.warning("Could not apply template to path. Using default output
|
|
800
|
+
logger.warning("Could not apply template to path. Using default output handling.")
|
|
604
801
|
# Fall back to default output handling
|
|
605
802
|
if guessed_name:
|
|
606
803
|
logger.debug("Using guessed name for output: %s", guessed_name)
|
|
@@ -615,7 +812,7 @@ def main():
|
|
|
615
812
|
out_filename = os.path.join(output_dir, guessed_name)
|
|
616
813
|
logger.debug("Using default output location: %s", out_filename)
|
|
617
814
|
else:
|
|
618
|
-
logger.warning("No metadata available to apply to template path. Using default output
|
|
815
|
+
logger.warning("No metadata available to apply to template path. Using default output handling.")
|
|
619
816
|
# Fall back to default output handling
|
|
620
817
|
elif guessed_name:
|
|
621
818
|
logger.debug("Using guessed name for output: %s", guessed_name)
|
|
@@ -208,6 +208,83 @@ def get_opus_tempfile(
|
|
|
208
208
|
return tmp_file, None
|
|
209
209
|
|
|
210
210
|
|
|
211
|
+
def convert_opus_to_mp3(
|
|
212
|
+
opus_data: bytes,
|
|
213
|
+
output_path: str,
|
|
214
|
+
ffmpeg_binary: str = None,
|
|
215
|
+
bitrate: int = 128,
|
|
216
|
+
auto_download: bool = False
|
|
217
|
+
) -> bool:
|
|
218
|
+
"""
|
|
219
|
+
Convert Opus audio data to MP3 format using FFmpeg.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
opus_data (bytes): Raw Opus audio data (OGG container with Opus codec)
|
|
223
|
+
output_path (str): Path where to save the MP3 file
|
|
224
|
+
ffmpeg_binary (str | None): Path to the ffmpeg binary. If None, will be auto-detected or downloaded.
|
|
225
|
+
bitrate (int): Bitrate for the MP3 encoding in kbps
|
|
226
|
+
auto_download (bool): Whether to automatically download dependencies if not found
|
|
227
|
+
Returns:
|
|
228
|
+
bool: True if conversion was successful, False otherwise
|
|
229
|
+
"""
|
|
230
|
+
logger.trace("Entering convert_opus_to_mp3(output_path=%s, bitrate=%d, auto_download=%s)",
|
|
231
|
+
output_path, bitrate, auto_download)
|
|
232
|
+
|
|
233
|
+
logger.debug("Converting Opus data to MP3 format (bitrate: %d kbps)", bitrate)
|
|
234
|
+
|
|
235
|
+
if ffmpeg_binary is None:
|
|
236
|
+
logger.debug("FFmpeg not specified, attempting to auto-detect")
|
|
237
|
+
ffmpeg_binary = get_ffmpeg_binary(auto_download)
|
|
238
|
+
if ffmpeg_binary is None:
|
|
239
|
+
logger.error("Could not find FFmpeg binary. Use --auto-download to enable automatic installation")
|
|
240
|
+
raise RuntimeError("Could not find FFmpeg binary. Use --auto-download to enable automatic installation")
|
|
241
|
+
logger.debug("Found FFmpeg at: %s", ffmpeg_binary)
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
logger.debug("Starting FFmpeg process for Opus to MP3 conversion")
|
|
245
|
+
ffmpeg_cmd = [
|
|
246
|
+
ffmpeg_binary, "-hide_banner", "-loglevel", "warning",
|
|
247
|
+
"-i", "-", # Read from stdin
|
|
248
|
+
"-acodec", "libmp3lame", # Use LAME MP3 encoder
|
|
249
|
+
"-b:a", f"{bitrate}k", # Set bitrate
|
|
250
|
+
"-y", # Overwrite output file
|
|
251
|
+
output_path
|
|
252
|
+
]
|
|
253
|
+
logger.trace("FFmpeg command: %s", ffmpeg_cmd)
|
|
254
|
+
|
|
255
|
+
ffmpeg_process = subprocess.Popen(
|
|
256
|
+
ffmpeg_cmd,
|
|
257
|
+
stdin=subprocess.PIPE,
|
|
258
|
+
stdout=subprocess.PIPE,
|
|
259
|
+
stderr=subprocess.PIPE
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Write Opus data to FFmpeg stdin
|
|
263
|
+
stdout, stderr = ffmpeg_process.communicate(input=opus_data)
|
|
264
|
+
|
|
265
|
+
logger.debug("FFmpeg process completed with return code: %d", ffmpeg_process.returncode)
|
|
266
|
+
|
|
267
|
+
if ffmpeg_process.returncode != 0:
|
|
268
|
+
logger.error("FFmpeg conversion failed with return code %d", ffmpeg_process.returncode)
|
|
269
|
+
if stderr:
|
|
270
|
+
logger.error("FFmpeg error output: %s", stderr.decode('utf-8', errors='replace'))
|
|
271
|
+
return False
|
|
272
|
+
|
|
273
|
+
# Verify the output file was created
|
|
274
|
+
if os.path.exists(output_path) and os.path.getsize(output_path) > 0:
|
|
275
|
+
logger.debug("Successfully created MP3 file: %s (size: %d bytes)",
|
|
276
|
+
output_path, os.path.getsize(output_path))
|
|
277
|
+
logger.trace("Exiting convert_opus_to_mp3() with success")
|
|
278
|
+
return True
|
|
279
|
+
else:
|
|
280
|
+
logger.error("MP3 file was not created or is empty: %s", output_path)
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
logger.error("Error during Opus to MP3 conversion: %s", str(e))
|
|
285
|
+
return False
|
|
286
|
+
|
|
287
|
+
|
|
211
288
|
def filter_directories(glob_list: list[str]) -> list[str]:
|
|
212
289
|
"""
|
|
213
290
|
Filter a list of glob results to include only audio files that can be handled by ffmpeg.
|
|
@@ -945,6 +945,66 @@ def get_ffmpeg_binary(auto_download=False):
|
|
|
945
945
|
logger.warning("FFmpeg is not available and --auto-download is not used.")
|
|
946
946
|
return None
|
|
947
947
|
|
|
948
|
+
def get_ffplay_binary(auto_download=False):
|
|
949
|
+
"""
|
|
950
|
+
Get the path to the FFplay binary, downloading it if necessary and allowed.
|
|
951
|
+
FFplay is typically included with FFmpeg installations.
|
|
952
|
+
|
|
953
|
+
Args:
|
|
954
|
+
auto_download (bool): Whether to automatically download FFplay if not found (defaults to False)
|
|
955
|
+
|
|
956
|
+
Returns:
|
|
957
|
+
str: Path to the FFplay binary, or None if not available
|
|
958
|
+
"""
|
|
959
|
+
logger.debug("Getting FFplay binary")
|
|
960
|
+
|
|
961
|
+
# Define the expected binary path
|
|
962
|
+
local_dir = os.path.join(get_user_data_dir(), 'libs', 'ffmpeg')
|
|
963
|
+
if sys.platform == 'win32':
|
|
964
|
+
binary_path = os.path.join(local_dir, 'ffplay.exe')
|
|
965
|
+
else:
|
|
966
|
+
binary_path = os.path.join(local_dir, 'ffplay')
|
|
967
|
+
|
|
968
|
+
# Check if binary exists
|
|
969
|
+
if os.path.exists(binary_path) and os.path.isfile(binary_path):
|
|
970
|
+
logger.debug("FFplay binary found at %s", binary_path)
|
|
971
|
+
return binary_path
|
|
972
|
+
|
|
973
|
+
# Check if a system-wide FFplay is available
|
|
974
|
+
try:
|
|
975
|
+
if sys.platform == 'win32':
|
|
976
|
+
# On Windows, look for ffplay in PATH
|
|
977
|
+
from shutil import which
|
|
978
|
+
system_binary = which('ffplay')
|
|
979
|
+
if system_binary:
|
|
980
|
+
logger.debug("System-wide FFplay found at %s", system_binary)
|
|
981
|
+
return system_binary
|
|
982
|
+
else:
|
|
983
|
+
# On Unix-like systems, use 'which' command
|
|
984
|
+
system_binary = subprocess.check_output(['which', 'ffplay']).decode('utf-8').strip()
|
|
985
|
+
if system_binary:
|
|
986
|
+
logger.debug("System-wide FFplay found at %s", system_binary)
|
|
987
|
+
return system_binary
|
|
988
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
|
989
|
+
logger.debug("No system-wide FFplay found")
|
|
990
|
+
|
|
991
|
+
# If FFplay is not found but auto_download is enabled, try to get FFmpeg first
|
|
992
|
+
# since FFplay is usually bundled with FFmpeg
|
|
993
|
+
if auto_download:
|
|
994
|
+
logger.info("FFplay not found, attempting to get FFmpeg (which includes FFplay)")
|
|
995
|
+
ffmpeg_path = get_ffmpeg_binary(auto_download=True)
|
|
996
|
+
if ffmpeg_path:
|
|
997
|
+
# Check if ffplay was included in the FFmpeg download
|
|
998
|
+
if os.path.exists(binary_path) and os.path.isfile(binary_path):
|
|
999
|
+
logger.info("FFplay found after FFmpeg download: %s", binary_path)
|
|
1000
|
+
return binary_path
|
|
1001
|
+
|
|
1002
|
+
logger.warning("FFplay not available even after FFmpeg download")
|
|
1003
|
+
return None
|
|
1004
|
+
else:
|
|
1005
|
+
logger.warning("FFplay is not available and --auto-download is not used.")
|
|
1006
|
+
return None
|
|
1007
|
+
|
|
948
1008
|
def get_opus_binary(auto_download=False):
|
|
949
1009
|
"""
|
|
950
1010
|
Get the path to the Opus binary, downloading it if necessary and allowed.
|
|
@@ -46,7 +46,31 @@ def handle_integration(args):
|
|
|
46
46
|
elif platform.system() == 'Darwin':
|
|
47
47
|
raise NotImplementedError("Context menu integration is not supported on MacOS YET. But Soon™")
|
|
48
48
|
elif platform.system() == 'Linux':
|
|
49
|
-
|
|
49
|
+
# Check if we're running in KDE
|
|
50
|
+
kde_session = os.environ.get('KDE_SESSION_VERSION') or os.environ.get('KDE_FULL_SESSION')
|
|
51
|
+
desktop_session = os.environ.get('DESKTOP_SESSION', '').lower()
|
|
52
|
+
xdg_current_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
|
|
53
|
+
|
|
54
|
+
if kde_session or 'kde' in desktop_session or 'kde' in xdg_current_desktop:
|
|
55
|
+
from .integration_kde import KDEServiceMenuIntegration as ContextMenuIntegration
|
|
56
|
+
if args.install_integration:
|
|
57
|
+
success = ContextMenuIntegration.install()
|
|
58
|
+
if success:
|
|
59
|
+
logger.info("Integration installed successfully.")
|
|
60
|
+
return True
|
|
61
|
+
else:
|
|
62
|
+
logger.error("Integration installation failed.")
|
|
63
|
+
return False
|
|
64
|
+
elif args.uninstall_integration:
|
|
65
|
+
success = ContextMenuIntegration.uninstall()
|
|
66
|
+
if success:
|
|
67
|
+
logger.info("Integration uninstalled successfully.")
|
|
68
|
+
return True
|
|
69
|
+
else:
|
|
70
|
+
logger.error("Integration uninstallation failed.")
|
|
71
|
+
return False
|
|
72
|
+
else:
|
|
73
|
+
raise NotImplementedError("Context menu integration is currently only supported on KDE. Other Linux desktop environments are not supported yet.")
|
|
50
74
|
else:
|
|
51
75
|
raise NotImplementedError(f"Context menu integration is not supported on this OS: {platform.system()}")
|
|
52
76
|
|
|
@@ -68,6 +92,18 @@ def handle_config():
|
|
|
68
92
|
context_menu._apply_config_template()
|
|
69
93
|
subprocess.call(["open", config_path])
|
|
70
94
|
elif platform.system() == "Linux":
|
|
95
|
+
kde_session = os.environ.get('KDE_SESSION_VERSION') or os.environ.get('KDE_FULL_SESSION')
|
|
96
|
+
desktop_session = os.environ.get('DESKTOP_SESSION', '').lower()
|
|
97
|
+
xdg_current_desktop = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
|
|
98
|
+
|
|
99
|
+
if kde_session or 'kde' in desktop_session or 'kde' in xdg_current_desktop:
|
|
100
|
+
try:
|
|
101
|
+
from .integration_kde import KDEServiceMenuIntegration as ContextMenuIntegration
|
|
102
|
+
context_menu = ContextMenuIntegration()
|
|
103
|
+
context_menu._apply_config_template()
|
|
104
|
+
except Exception as e:
|
|
105
|
+
logger.warning(f"Could not create config template: {e}")
|
|
106
|
+
|
|
71
107
|
subprocess.call(["xdg-open", config_path])
|
|
72
108
|
else:
|
|
73
109
|
logger.error(f"Unsupported OS: {platform.system()}")
|