TonieToolbox 0.4.1__py3-none-any.whl → 0.5.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.
- TonieToolbox/__init__.py +1 -1
- TonieToolbox/__main__.py +269 -349
- TonieToolbox/artwork.py +105 -0
- TonieToolbox/audio_conversion.py +48 -5
- TonieToolbox/media_tags.py +5 -4
- TonieToolbox/recursive_processor.py +24 -19
- TonieToolbox/tags.py +74 -0
- TonieToolbox/teddycloud.py +250 -593
- TonieToolbox/tonie_analysis.py +173 -13
- TonieToolbox/tonie_file.py +17 -29
- TonieToolbox/tonies_json.py +1036 -170
- TonieToolbox/version_handler.py +26 -22
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0.dist-info}/METADATA +147 -99
- tonietoolbox-0.5.0.dist-info/RECORD +26 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0.dist-info}/WHEEL +1 -1
- tonietoolbox-0.4.1.dist-info/RECORD +0 -24
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0.dist-info}/entry_points.txt +0 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0.dist-info}/licenses/LICENSE.md +0 -0
- {tonietoolbox-0.4.1.dist-info → tonietoolbox-0.5.0.dist-info}/top_level.txt +0 -0
TonieToolbox/__main__.py
CHANGED
@@ -7,27 +7,27 @@ import argparse
|
|
7
7
|
import os
|
8
8
|
import sys
|
9
9
|
import logging
|
10
|
-
|
11
10
|
from . import __version__
|
12
11
|
from .audio_conversion import get_input_files, append_to_filename
|
13
12
|
from .tonie_file import create_tonie_file
|
14
|
-
from .tonie_analysis import check_tonie_file, split_to_opus_files
|
13
|
+
from .tonie_analysis import check_tonie_file, check_tonie_file_cli, split_to_opus_files, compare_taf_files
|
15
14
|
from .dependency_manager import get_ffmpeg_binary, get_opus_binary
|
16
|
-
from .logger import setup_logging, get_logger
|
15
|
+
from .logger import TRACE, setup_logging, get_logger
|
17
16
|
from .filename_generator import guess_output_filename
|
18
17
|
from .version_handler import check_for_updates, clear_version_cache
|
19
18
|
from .recursive_processor import process_recursive_folders
|
20
|
-
from .media_tags import is_available as is_media_tags_available, ensure_mutagen
|
21
|
-
from .teddycloud import
|
22
|
-
from .
|
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 .teddycloud import TeddyCloudClient
|
21
|
+
from .tags import get_tags
|
22
|
+
from .tonies_json import fetch_and_update_tonies_json_v1, fetch_and_update_tonies_json_v2
|
23
|
+
from .artwork import upload_artwork
|
23
24
|
|
24
25
|
def main():
|
25
26
|
"""Entry point for the TonieToolbox application."""
|
26
27
|
parser = argparse.ArgumentParser(description='Create Tonie compatible file from Ogg opus file(s).')
|
27
28
|
parser.add_argument('-v', '--version', action='version', version=f'TonieToolbox {__version__}',
|
28
|
-
help='show program version and exit')
|
29
|
-
|
30
|
-
# TeddyCloud options first to check for existence before requiring SOURCE
|
29
|
+
help='show program version and exit')
|
30
|
+
# ------------- Parser - Teddycloud -------------
|
31
31
|
teddycloud_group = parser.add_argument_group('TeddyCloud Options')
|
32
32
|
teddycloud_group.add_argument('--upload', metavar='URL', action='store',
|
33
33
|
help='Upload to TeddyCloud instance (e.g., https://teddycloud.example.com). Supports .taf, .jpg, .jpeg, .png files.')
|
@@ -41,8 +41,6 @@ def main():
|
|
41
41
|
help='Special folder to upload to (currently only "library" is supported)', default='library')
|
42
42
|
teddycloud_group.add_argument('--path', action='store', metavar='PATH',
|
43
43
|
help='Path where to write the file on TeddyCloud server')
|
44
|
-
teddycloud_group.add_argument('--show-progress', action='store_true', default=True,
|
45
|
-
help='Show progress bar during file upload (default: enabled)')
|
46
44
|
teddycloud_group.add_argument('--connection-timeout', type=int, metavar='SECONDS', default=10,
|
47
45
|
help='Connection timeout in seconds (default: 10)')
|
48
46
|
teddycloud_group.add_argument('--read-timeout', type=int, metavar='SECONDS', default=300,
|
@@ -53,18 +51,33 @@ def main():
|
|
53
51
|
help='Delay between retry attempts in seconds (default: 5)')
|
54
52
|
teddycloud_group.add_argument('--create-custom-json', action='store_true',
|
55
53
|
help='Fetch and update custom Tonies JSON data')
|
54
|
+
teddycloud_group.add_argument('--version-2', action='store_true',
|
55
|
+
help='Use version 2 of the Tonies JSON format (default: version 1)')
|
56
|
+
# ------------- Parser - Authentication options for TeddyCloud -------------
|
57
|
+
teddycloud_group.add_argument('--username', action='store', metavar='USERNAME',
|
58
|
+
help='Username for basic authentication')
|
59
|
+
teddycloud_group.add_argument('--password', action='store', metavar='PASSWORD',
|
60
|
+
help='Password for basic authentication')
|
61
|
+
teddycloud_group.add_argument('--client-cert', action='store', metavar='CERT_FILE',
|
62
|
+
help='Path to client certificate file for certificate-based authentication')
|
63
|
+
teddycloud_group.add_argument('--client-key', action='store', metavar='KEY_FILE',
|
64
|
+
help='Path to client private key file for certificate-based authentication')
|
56
65
|
|
66
|
+
# ------------- Parser - Source Input -------------
|
57
67
|
parser.add_argument('input_filename', metavar='SOURCE', type=str, nargs='?',
|
58
68
|
help='input file or directory or a file list (.lst)')
|
59
69
|
parser.add_argument('output_filename', metavar='TARGET', nargs='?', type=str,
|
60
70
|
help='the output file name (default: ---ID---)')
|
61
71
|
parser.add_argument('-t', '--timestamp', dest='user_timestamp', metavar='TIMESTAMP', action='store',
|
62
72
|
help='set custom timestamp / bitstream serial')
|
63
|
-
|
73
|
+
# ------------- Parser - Librarys -------------
|
64
74
|
parser.add_argument('-f', '--ffmpeg', help='specify location of ffmpeg', default=None)
|
65
75
|
parser.add_argument('-o', '--opusenc', help='specify location of opusenc', default=None)
|
66
76
|
parser.add_argument('-b', '--bitrate', type=int, help='set encoding bitrate in kbps (default: 96)', default=96)
|
67
77
|
parser.add_argument('-c', '--cbr', action='store_true', help='encode in cbr mode')
|
78
|
+
parser.add_argument('--auto-download', action='store_true',
|
79
|
+
help='automatically download ffmpeg and opusenc if not found')
|
80
|
+
# ------------- Parser - TAF -------------
|
68
81
|
parser.add_argument('-a', '--append-tonie-tag', metavar='TAG', action='store',
|
69
82
|
help='append [TAG] to filename (must be an 8-character hex value)')
|
70
83
|
parser.add_argument('-n', '--no-tonie-header', action='store_true', help='do not write Tonie header')
|
@@ -73,7 +86,11 @@ def main():
|
|
73
86
|
parser.add_argument('-r', '--recursive', action='store_true', help='Process folders recursively')
|
74
87
|
parser.add_argument('-O', '--output-to-source', action='store_true',
|
75
88
|
help='Save output files in the source directory instead of output directory')
|
76
|
-
parser.add_argument('-
|
89
|
+
parser.add_argument('-fc', '--force-creation', action='store_true', default=False,
|
90
|
+
help='Force creation of Tonie file even if it already exists')
|
91
|
+
parser.add_argument('--no-mono-conversion', action='store_true',
|
92
|
+
help='Do not convert mono audio to stereo (default: convert mono to stereo)')
|
93
|
+
# ------------- Parser - Debug TAFs -------------
|
77
94
|
parser.add_argument('-k', '--keep-temp', action='store_true',
|
78
95
|
help='Keep temporary opus files in a temp folder for testing')
|
79
96
|
parser.add_argument('-u', '--use-legacy-tags', action='store_true',
|
@@ -81,9 +98,8 @@ def main():
|
|
81
98
|
parser.add_argument('-C', '--compare', action='store', metavar='FILE2',
|
82
99
|
help='Compare input file with another .taf file for debugging')
|
83
100
|
parser.add_argument('-D', '--detailed-compare', action='store_true',
|
84
|
-
help='Show detailed OGG page differences when comparing files')
|
85
|
-
|
86
|
-
# Media tag options
|
101
|
+
help='Show detailed OGG page differences when comparing files')
|
102
|
+
# ------------- Parser - Media Tag Options -------------
|
87
103
|
media_tag_group = parser.add_argument_group('Media Tag Options')
|
88
104
|
media_tag_group.add_argument('-m', '--use-media-tags', action='store_true',
|
89
105
|
help='Use media tags from audio files for naming')
|
@@ -91,8 +107,7 @@ def main():
|
|
91
107
|
help='Template for naming files using media tags. Example: "{album} - {artist}"')
|
92
108
|
media_tag_group.add_argument('--show-tags', action='store_true',
|
93
109
|
help='Show available media tags from input files')
|
94
|
-
|
95
|
-
# Version check options
|
110
|
+
# ------------- Parser - Version handling -------------
|
96
111
|
version_group = parser.add_argument_group('Version Check Options')
|
97
112
|
version_group.add_argument('-S', '--skip-update-check', action='store_true',
|
98
113
|
help='Skip checking for updates')
|
@@ -100,7 +115,7 @@ def main():
|
|
100
115
|
help='Force refresh of update information from PyPI')
|
101
116
|
version_group.add_argument('-X', '--clear-version-cache', action='store_true',
|
102
117
|
help='Clear cached version information')
|
103
|
-
|
118
|
+
# ------------- Parser - Logging -------------
|
104
119
|
log_group = parser.add_argument_group('Logging Options')
|
105
120
|
log_level_group = log_group.add_mutually_exclusive_group()
|
106
121
|
log_level_group.add_argument('-d', '--debug', action='store_true', help='Enable debug logging')
|
@@ -109,16 +124,14 @@ def main():
|
|
109
124
|
log_level_group.add_argument('-Q', '--silent', action='store_true', help='Show only errors')
|
110
125
|
log_group.add_argument('--log-file', action='store_true', default=False,
|
111
126
|
help='Save logs to a timestamped file in .tonietoolbox folder')
|
112
|
-
|
113
127
|
args = parser.parse_args()
|
114
128
|
|
115
|
-
#
|
129
|
+
# ------------- Parser - Source Input -------------
|
116
130
|
if args.input_filename is None and not (args.get_tags or args.upload):
|
117
131
|
parser.error("the following arguments are required: SOURCE")
|
118
|
-
|
119
|
-
#
|
132
|
+
|
133
|
+
# ------------- Logging -------------
|
120
134
|
if args.trace:
|
121
|
-
from .logger import TRACE
|
122
135
|
log_level = TRACE
|
123
136
|
elif args.debug:
|
124
137
|
log_level = logging.DEBUG
|
@@ -127,17 +140,15 @@ def main():
|
|
127
140
|
elif args.silent:
|
128
141
|
log_level = logging.ERROR
|
129
142
|
else:
|
130
|
-
log_level = logging.INFO
|
131
|
-
|
143
|
+
log_level = logging.INFO
|
132
144
|
setup_logging(log_level, log_to_file=args.log_file)
|
133
145
|
logger = get_logger('main')
|
134
146
|
logger.debug("Starting TonieToolbox v%s with log level: %s", __version__, logging.getLevelName(log_level))
|
135
|
-
|
136
|
-
# Log the command-line arguments at trace level for debugging purposes
|
137
|
-
logger.log(logging.DEBUG - 1, "Command-line arguments: %s", vars(args))
|
147
|
+
logger.debug("Command-line arguments: %s", vars(args))
|
138
148
|
|
149
|
+
# ------------- Version handling -------------
|
139
150
|
if args.clear_version_cache:
|
140
|
-
logger.
|
151
|
+
logger.debug("Clearing version cache")
|
141
152
|
if clear_version_cache():
|
142
153
|
logger.info("Version cache cleared successfully")
|
143
154
|
else:
|
@@ -150,73 +161,155 @@ def main():
|
|
150
161
|
force_refresh=args.force_refresh_cache
|
151
162
|
)
|
152
163
|
|
153
|
-
logger.
|
164
|
+
logger.debug( "Update check results: is_latest=%s, latest_version=%s, update_confirmed=%s",
|
154
165
|
is_latest, latest_version, update_confirmed)
|
155
166
|
|
156
167
|
if not is_latest and not update_confirmed and not (args.silent or args.quiet):
|
157
168
|
logger.info("Update available but user chose to continue without updating.")
|
158
169
|
|
159
|
-
|
160
|
-
if args.
|
161
|
-
logger.debug("
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
170
|
+
# ------------- Normalize Path Input -------------
|
171
|
+
if args.input_filename:
|
172
|
+
logger.debug("Original input path: %s", args.input_filename)
|
173
|
+
# Strip quotes from the beginning and end
|
174
|
+
args.input_filename = args.input_filename.strip('"\'')
|
175
|
+
# Handle paths that end with a backslash
|
176
|
+
if args.input_filename.endswith('\\'):
|
177
|
+
args.input_filename = args.input_filename.rstrip('\\')
|
178
|
+
logger.debug("Normalized input path: %s", args.input_filename)
|
179
|
+
|
180
|
+
# ------------- Setup TeddyCloudClient-------------
|
181
|
+
if args.upload or args.get_tags:
|
182
|
+
if args.upload:
|
183
|
+
teddycloud_url = args.upload
|
184
|
+
elif args.get_tags:
|
185
|
+
teddycloud_url = args.get_tags
|
186
|
+
if not teddycloud_url:
|
187
|
+
logger.error("TeddyCloud URL is required for --upload or --get-tags")
|
188
|
+
sys.exit(1)
|
189
|
+
try:
|
190
|
+
client = TeddyCloudClient(
|
191
|
+
base_url=teddycloud_url,
|
192
|
+
ignore_ssl_verify=args.ignore_ssl_verify,
|
193
|
+
username=args.username,
|
194
|
+
password=args.password,
|
195
|
+
cert_file=args.client_cert,
|
196
|
+
key_file=args.client_key
|
197
|
+
)
|
198
|
+
logger.debug("TeddyCloud client initialized successfully")
|
199
|
+
except Exception as e:
|
200
|
+
logger.error("Failed to initialize TeddyCloud client: %s", str(e))
|
201
|
+
sys.exit(1)
|
202
|
+
|
203
|
+
if args.get_tags:
|
204
|
+
logger.debug("Getting tags from TeddyCloud: %s", teddycloud_url)
|
205
|
+
success = get_tags(client)
|
206
|
+
logger.debug( "Exiting with code %d", 0 if success else 1)
|
207
|
+
sys.exit(0 if success else 1)
|
166
208
|
|
167
|
-
#
|
168
|
-
if args.
|
169
|
-
|
170
|
-
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
209
|
+
# ------------- Show Media Tags -------------
|
210
|
+
if args.show_tags:
|
211
|
+
logger.info("Showing media tags for input files:")
|
171
212
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
213
|
+
for file_index, file_path in enumerate(files):
|
214
|
+
tags = get_file_tags(file_path)
|
215
|
+
if tags:
|
216
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
217
|
+
print("-" * 40)
|
218
|
+
for tag_name, tag_value in sorted(tags.items()):
|
219
|
+
print(f"{tag_name}: {tag_value}")
|
220
|
+
else:
|
221
|
+
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
222
|
+
sys.exit(0)
|
223
|
+
# ------------- Direct Upload -------------
|
224
|
+
if os.path.exists(args.input_filename) and os.path.isfile(args.input_filename):
|
225
|
+
file_path = args.input_filename
|
226
|
+
file_size = os.path.getsize(file_path)
|
227
|
+
file_ext = os.path.splitext(file_path)[1].lower()
|
228
|
+
|
229
|
+
if args.upload and not args.recursive and file_ext == '.taf':
|
230
|
+
logger.debug("Upload to TeddyCloud requested: %s", teddycloud_url)
|
231
|
+
logger.trace("TeddyCloud upload parameters: path=%s, special_folder=%s, ignore_ssl=%s",
|
232
|
+
args.path, args.special_folder, args.ignore_ssl_verify)
|
233
|
+
|
234
|
+
|
235
|
+
logger.debug("File to upload: %s (size: %d bytes, type: %s)",
|
236
|
+
file_path, file_size, file_ext)
|
196
237
|
logger.info("Uploading %s to TeddyCloud %s", file_path, teddycloud_url)
|
197
|
-
|
198
|
-
|
199
|
-
args.
|
200
|
-
|
201
|
-
args.
|
238
|
+
logger.trace("Starting upload process for %s", file_path)
|
239
|
+
response = client.upload_file(
|
240
|
+
destination_path=args.path,
|
241
|
+
file_path=file_path,
|
242
|
+
special=args.special_folder,
|
202
243
|
)
|
203
|
-
|
244
|
+
logger.trace("Upload response received: %s", response)
|
245
|
+
upload_success = response.get('success', False)
|
204
246
|
if not upload_success:
|
205
|
-
|
247
|
+
error_msg = response.get('message', 'Unknown error')
|
248
|
+
logger.error("Failed to upload %s to TeddyCloud: %s (HTTP Status: %s, Response: %s)",
|
249
|
+
file_path, error_msg, response.get('status_code', 'Unknown'), response)
|
250
|
+
logger.trace("Exiting with code 1 due to upload failure")
|
206
251
|
sys.exit(1)
|
207
252
|
else:
|
208
253
|
logger.info("Successfully uploaded %s to TeddyCloud", file_path)
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
254
|
+
logger.debug("Upload response details: %s",
|
255
|
+
{k: v for k, v in response.items() if k != 'success'})
|
256
|
+
artwork_url = None
|
257
|
+
if args.include_artwork and file_path.lower().endswith('.taf'):
|
258
|
+
source_dir = os.path.dirname(file_path)
|
259
|
+
logger.info("Looking for artwork to upload for %s", file_path)
|
260
|
+
logger.debug("Searching for artwork in directory: %s", source_dir)
|
261
|
+
logger.trace("Calling upload_artwork function")
|
262
|
+
success, artwork_url = upload_artwork(client, file_path, source_dir, [])
|
263
|
+
logger.trace("upload_artwork returned: success=%s, artwork_url=%s",
|
264
|
+
success, artwork_url)
|
265
|
+
if success:
|
266
|
+
logger.info("Successfully uploaded artwork for %s", file_path)
|
267
|
+
logger.debug("Artwork URL: %s", artwork_url)
|
268
|
+
else:
|
269
|
+
logger.warning("Failed to upload artwork for %s", file_path)
|
270
|
+
logger.debug("No suitable artwork found or upload failed")
|
271
|
+
if args.create_custom_json and file_path.lower().endswith('.taf'):
|
272
|
+
output_dir = './output'
|
273
|
+
logger.debug("Creating/ensuring output directory for JSON: %s", output_dir)
|
274
|
+
if not os.path.exists(output_dir):
|
275
|
+
os.makedirs(output_dir, exist_ok=True)
|
276
|
+
logger.trace("Created output directory: %s", output_dir)
|
277
|
+
logger.debug("Updating tonies.custom.json with: taf=%s, artwork_url=%s",
|
278
|
+
file_path, artwork_url)
|
279
|
+
client_param = client
|
217
280
|
|
281
|
+
if args.version_2:
|
282
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
283
|
+
success = fetch_and_update_tonies_json_v2(client_param, file_path, [], artwork_url, output_dir)
|
284
|
+
else:
|
285
|
+
success = fetch_and_update_tonies_json_v1(client_param, file_path, [], artwork_url, output_dir)
|
286
|
+
if success:
|
287
|
+
logger.info("Successfully updated Tonies JSON for %s", file_path)
|
288
|
+
else:
|
289
|
+
logger.warning("Failed to update Tonies JSON for %s", file_path)
|
290
|
+
logger.debug("fetch_and_update_tonies_json returned failure")
|
291
|
+
logger.trace("Exiting after direct upload with code 0")
|
292
|
+
sys.exit(0)
|
293
|
+
elif not args.recursive:
|
294
|
+
if not os.path.exists(args.input_filename):
|
295
|
+
logger.error("File not found: %s", args.input_filename)
|
296
|
+
elif not os.path.isfile(args.input_filename):
|
297
|
+
logger.error("Not a regular file: %s", args.input_filename)
|
298
|
+
logger.debug("File exists: %s, Is file: %s",
|
299
|
+
os.path.exists(args.input_filename),
|
300
|
+
os.path.isfile(args.input_filename) if os.path.exists(args.input_filename) else False)
|
301
|
+
logger.trace("Exiting with code 1 due to invalid input file")
|
302
|
+
sys.exit(1)
|
303
|
+
|
304
|
+
if args.recursive and args.upload:
|
305
|
+
logger.info("Recursive mode with upload enabled: %s -> %s", args.input_filename, teddycloud_url)
|
306
|
+
logger.debug("Will process all files in directory recursively and upload to TeddyCloud")
|
307
|
+
|
308
|
+
# ------------- Librarys / Prereqs -------------
|
309
|
+
logger.debug("Checking for external dependencies")
|
218
310
|
ffmpeg_binary = args.ffmpeg
|
219
311
|
if ffmpeg_binary is None:
|
312
|
+
logger.debug("No FFmpeg specified, attempting to locate binary (auto_download=%s)", args.auto_download)
|
220
313
|
ffmpeg_binary = get_ffmpeg_binary(args.auto_download)
|
221
314
|
if ffmpeg_binary is None:
|
222
315
|
logger.error("Could not find FFmpeg. Please install FFmpeg or specify its location using --ffmpeg or use --auto-download")
|
@@ -225,13 +318,13 @@ def main():
|
|
225
318
|
|
226
319
|
opus_binary = args.opusenc
|
227
320
|
if opus_binary is None:
|
321
|
+
logger.debug("No opusenc specified, attempting to locate binary (auto_download=%s)", args.auto_download)
|
228
322
|
opus_binary = get_opus_binary(args.auto_download)
|
229
323
|
if opus_binary is None:
|
230
324
|
logger.error("Could not find opusenc. Please install opus-tools or specify its location using --opusenc or use --auto-download")
|
231
325
|
sys.exit(1)
|
232
326
|
logger.debug("Using opusenc binary: %s", opus_binary)
|
233
327
|
|
234
|
-
# Check for media tags library and handle --show-tags option
|
235
328
|
if (args.use_media_tags or args.show_tags or args.name_template) and not is_media_tags_available():
|
236
329
|
if not ensure_mutagen(auto_install=args.auto_download):
|
237
330
|
logger.warning("Media tags functionality requires the mutagen library but it could not be installed.")
|
@@ -240,8 +333,8 @@ def main():
|
|
240
333
|
sys.exit(1)
|
241
334
|
else:
|
242
335
|
logger.info("Successfully enabled media tag support")
|
243
|
-
|
244
|
-
#
|
336
|
+
|
337
|
+
# ------------- Recursive Processing -------------
|
245
338
|
if args.recursive:
|
246
339
|
logger.info("Processing folders recursively: %s", args.input_filename)
|
247
340
|
process_tasks = process_recursive_folders(
|
@@ -266,113 +359,75 @@ def main():
|
|
266
359
|
task_out_filename = os.path.join(folder_path, f"{output_name}.taf")
|
267
360
|
else:
|
268
361
|
task_out_filename = os.path.join(output_dir, f"{output_name}.taf")
|
269
|
-
|
362
|
+
|
363
|
+
skip_creation = False
|
364
|
+
if os.path.exists(task_out_filename):
|
365
|
+
logger.warning("Output file already exists: %s", task_out_filename)
|
366
|
+
valid_taf = check_tonie_file_cli(task_out_filename)
|
367
|
+
|
368
|
+
if valid_taf and not args.force_creation:
|
369
|
+
logger.warning("Valid Tonie file: %s", task_out_filename)
|
370
|
+
logger.warning("Skipping creation step for existing Tonie file: %s", task_out_filename)
|
371
|
+
skip_creation = True
|
372
|
+
else:
|
373
|
+
logger.info("Output file exists but is not a valid Tonie file, proceeding to create a new one.")
|
374
|
+
|
270
375
|
logger.info("[%d/%d] Processing folder: %s -> %s",
|
271
376
|
task_index + 1, len(process_tasks), folder_path, task_out_filename)
|
272
377
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
378
|
+
if not skip_creation:
|
379
|
+
create_tonie_file(task_out_filename, audio_files, args.no_tonie_header, args.user_timestamp,
|
380
|
+
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
381
|
+
args.auto_download, not args.use_legacy_tags,
|
382
|
+
no_mono_conversion=args.no_mono_conversion)
|
383
|
+
logger.info("Successfully created Tonie file: %s", task_out_filename)
|
278
384
|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
385
|
+
created_files.append(task_out_filename)
|
386
|
+
|
387
|
+
# ------------- Initialization -------------------
|
388
|
+
|
389
|
+
artwork_url = None
|
284
390
|
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
args.
|
290
|
-
args.
|
391
|
+
# ------------- Recursive File Upload -------------
|
392
|
+
if args.upload:
|
393
|
+
response = client.upload_file(
|
394
|
+
file_path=task_out_filename,
|
395
|
+
destination_path=args.path,
|
396
|
+
special=args.special_folder,
|
291
397
|
)
|
398
|
+
upload_success = response.get('success', False)
|
292
399
|
|
293
400
|
if not upload_success:
|
294
|
-
logger.error("Failed to upload %s to TeddyCloud",
|
401
|
+
logger.error("Failed to upload %s to TeddyCloud", task_out_filename)
|
295
402
|
else:
|
296
|
-
logger.info("Successfully uploaded %s to TeddyCloud",
|
403
|
+
logger.info("Successfully uploaded %s to TeddyCloud", task_out_filename)
|
297
404
|
|
298
|
-
# Handle artwork upload
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
logger.
|
305
|
-
|
306
|
-
# Try to find cover image in the folder
|
307
|
-
from .media_tags import find_cover_image
|
308
|
-
artwork_path = find_cover_image(folder_path)
|
309
|
-
temp_artwork = None
|
310
|
-
|
311
|
-
# If no cover image found, try to extract it from one of the audio files
|
312
|
-
if not artwork_path:
|
313
|
-
# Get current task's audio files
|
314
|
-
for task_name, task_folder, task_files in process_tasks:
|
315
|
-
if task_folder == folder_path or os.path.normpath(task_folder) == os.path.normpath(folder_path):
|
316
|
-
if task_files and len(task_files) > 0:
|
317
|
-
# Try to extract from first file
|
318
|
-
from .media_tags import extract_artwork, ensure_mutagen
|
319
|
-
if ensure_mutagen(auto_install=args.auto_download):
|
320
|
-
temp_artwork = extract_artwork(task_files[0])
|
321
|
-
if temp_artwork:
|
322
|
-
artwork_path = temp_artwork
|
323
|
-
break
|
324
|
-
|
325
|
-
if artwork_path:
|
326
|
-
logger.info("Found artwork for %s: %s", folder_path, artwork_path)
|
327
|
-
artwork_upload_path = "/custom_img"
|
328
|
-
artwork_ext = os.path.splitext(artwork_path)[1]
|
405
|
+
# Handle artwork upload
|
406
|
+
if args.include_artwork:
|
407
|
+
success, artwork_url = upload_artwork(client, task_out_filename, folder_path, audio_files)
|
408
|
+
if success:
|
409
|
+
logger.info("Successfully uploaded artwork for %s", task_out_filename)
|
410
|
+
else:
|
411
|
+
logger.warning("Failed to upload artwork for %s", task_out_filename)
|
329
412
|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
args.special_folder, artwork_upload_path, args.show_progress,
|
347
|
-
args.connection_timeout, args.read_timeout,
|
348
|
-
args.max_retries, args.retry_delay
|
349
|
-
)
|
350
|
-
|
351
|
-
if artwork_upload_success:
|
352
|
-
logger.info("Successfully uploaded artwork for %s", folder_path)
|
353
|
-
else:
|
354
|
-
logger.warning("Failed to upload artwork for %s", folder_path)
|
355
|
-
|
356
|
-
if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
|
357
|
-
try:
|
358
|
-
os.unlink(renamed_artwork_path)
|
359
|
-
logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
|
360
|
-
except Exception as e:
|
361
|
-
logger.debug("Failed to remove temporary renamed artwork file: %s", e)
|
362
|
-
|
363
|
-
if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
|
364
|
-
try:
|
365
|
-
os.unlink(temp_artwork)
|
366
|
-
logger.debug("Removed temporary artwork file: %s", temp_artwork)
|
367
|
-
except Exception as e:
|
368
|
-
logger.debug("Failed to remove temporary artwork file: %s", e)
|
369
|
-
except Exception as e:
|
370
|
-
logger.error("Error during artwork renaming or upload: %s", e)
|
371
|
-
else:
|
372
|
-
logger.warning("No artwork found for %s", folder_path)
|
413
|
+
# tonies.custom.json generation
|
414
|
+
if args.create_custom_json:
|
415
|
+
base_path = os.path.dirname(args.input_filename)
|
416
|
+
json_output_dir = base_path if args.output_to_source else output_dir
|
417
|
+
client_param = client if 'client' in locals() else None
|
418
|
+
if args.version_2:
|
419
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
420
|
+
success = fetch_and_update_tonies_json_v2(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
|
421
|
+
else:
|
422
|
+
success = fetch_and_update_tonies_json_v1(client_param, task_out_filename, audio_files, artwork_url, json_output_dir)
|
423
|
+
if success:
|
424
|
+
logger.info("Successfully updated Tonies JSON for %s", task_out_filename)
|
425
|
+
else:
|
426
|
+
logger.warning("Failed to update Tonies JSON for %s", task_out_filename)
|
427
|
+
|
428
|
+
logger.info("Recursive processing completed. Created %d Tonie files.", len(process_tasks))
|
373
429
|
sys.exit(0)
|
374
|
-
|
375
|
-
# Handle directory or file input
|
430
|
+
# ------------- Single File Processing -------------
|
376
431
|
if os.path.isdir(args.input_filename):
|
377
432
|
logger.debug("Input is a directory: %s", args.input_filename)
|
378
433
|
args.input_filename += "/*"
|
@@ -387,7 +442,6 @@ def main():
|
|
387
442
|
split_to_opus_files(args.input_filename, args.output_filename)
|
388
443
|
sys.exit(0)
|
389
444
|
elif args.compare:
|
390
|
-
from .tonie_analysis import compare_taf_files
|
391
445
|
logger.info("Comparing Tonie files: %s and %s", args.input_filename, args.compare)
|
392
446
|
result = compare_taf_files(args.input_filename, args.compare, args.detailed_compare)
|
393
447
|
sys.exit(0 if result else 1)
|
@@ -398,36 +452,14 @@ def main():
|
|
398
452
|
if len(files) == 0:
|
399
453
|
logger.error("No files found for pattern %s", args.input_filename)
|
400
454
|
sys.exit(1)
|
401
|
-
|
402
|
-
# Show tags for input files if requested
|
403
|
-
if args.show_tags:
|
404
|
-
from .media_tags import get_file_tags
|
405
|
-
logger.info("Showing media tags for input files:")
|
406
|
-
|
407
|
-
for file_index, file_path in enumerate(files):
|
408
|
-
tags = get_file_tags(file_path)
|
409
|
-
if tags:
|
410
|
-
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)}")
|
411
|
-
print("-" * 40)
|
412
|
-
for tag_name, tag_value in sorted(tags.items()):
|
413
|
-
print(f"{tag_name}: {tag_value}")
|
414
|
-
else:
|
415
|
-
print(f"\nFile {file_index + 1}: {os.path.basename(file_path)} - No tags found")
|
416
|
-
sys.exit(0)
|
417
|
-
|
418
|
-
# Use media tags for file naming if requested
|
455
|
+
|
419
456
|
guessed_name = None
|
420
457
|
if args.use_media_tags:
|
421
|
-
# If this is a single folder, try to get consistent album info
|
422
458
|
if len(files) > 1 and os.path.dirname(files[0]) == os.path.dirname(files[-1]):
|
423
|
-
folder_path = os.path.dirname(files[0])
|
424
|
-
|
425
|
-
from .media_tags import extract_album_info, format_metadata_filename
|
426
|
-
logger.debug("Extracting album info from folder: %s", folder_path)
|
427
|
-
|
459
|
+
folder_path = os.path.dirname(files[0])
|
460
|
+
logger.debug("Extracting album info from folder: %s", folder_path)
|
428
461
|
album_info = extract_album_info(folder_path)
|
429
462
|
if album_info:
|
430
|
-
# Use album info for naming the output file
|
431
463
|
template = args.name_template or "{album} - {artist}"
|
432
464
|
new_name = format_metadata_filename(album_info, template)
|
433
465
|
|
@@ -436,10 +468,8 @@ def main():
|
|
436
468
|
guessed_name = new_name
|
437
469
|
else:
|
438
470
|
logger.debug("Could not format filename from album metadata")
|
439
|
-
|
440
|
-
# For single files, use the file's metadata
|
441
471
|
elif len(files) == 1:
|
442
|
-
|
472
|
+
|
443
473
|
|
444
474
|
tags = get_file_tags(files[0])
|
445
475
|
if tags:
|
@@ -453,9 +483,7 @@ def main():
|
|
453
483
|
logger.debug("Could not format filename from file metadata")
|
454
484
|
|
455
485
|
# For multiple files from different folders, try to use common tags if they exist
|
456
|
-
elif len(files) > 1:
|
457
|
-
from .media_tags import get_file_tags, format_metadata_filename
|
458
|
-
|
486
|
+
elif len(files) > 1:
|
459
487
|
# Try to find common tags among files
|
460
488
|
common_tags = {}
|
461
489
|
for file_path in files:
|
@@ -509,6 +537,9 @@ def main():
|
|
509
537
|
os.makedirs(output_dir, exist_ok=True)
|
510
538
|
out_filename = os.path.join(output_dir, guessed_name)
|
511
539
|
logger.debug("Using default output location: %s", out_filename)
|
540
|
+
|
541
|
+
# Make sure source_dir is defined for later use with artwork upload
|
542
|
+
source_dir = os.path.dirname(files[0]) if files else '.'
|
512
543
|
|
513
544
|
if args.append_tonie_tag:
|
514
545
|
logger.debug("Appending Tonie tag to output filename")
|
@@ -526,155 +557,44 @@ def main():
|
|
526
557
|
logger.info("Creating Tonie file: %s with %d input file(s)", out_filename, len(files))
|
527
558
|
create_tonie_file(out_filename, files, args.no_tonie_header, args.user_timestamp,
|
528
559
|
args.bitrate, not args.cbr, ffmpeg_binary, opus_binary, args.keep_temp,
|
529
|
-
args.auto_download, not args.use_legacy_tags
|
560
|
+
args.auto_download, not args.use_legacy_tags,
|
561
|
+
no_mono_conversion=args.no_mono_conversion)
|
530
562
|
logger.info("Successfully created Tonie file: %s", out_filename)
|
531
563
|
|
532
|
-
#
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
args.special_folder,
|
539
|
-
args.connection_timeout, args.read_timeout,
|
540
|
-
args.max_retries, args.retry_delay
|
564
|
+
# ------------- Single File Upload -------------
|
565
|
+
artwork_url = None
|
566
|
+
if args.upload:
|
567
|
+
response = client.upload_file(
|
568
|
+
file_path=out_filename,
|
569
|
+
destination_path=args.path,
|
570
|
+
special=args.special_folder,
|
541
571
|
)
|
572
|
+
upload_success = response.get('success', False)
|
542
573
|
if not upload_success:
|
543
574
|
logger.error("Failed to upload %s to TeddyCloud", out_filename)
|
544
|
-
sys.exit(1)
|
545
575
|
else:
|
546
576
|
logger.info("Successfully uploaded %s to TeddyCloud", out_filename)
|
547
|
-
|
548
|
-
# Handle artwork upload if requested
|
549
|
-
if args.include_artwork:
|
550
|
-
logger.info("Looking for artwork to upload alongside the Tonie file")
|
551
|
-
artwork_path = None
|
552
|
-
|
553
|
-
# Try to find a cover image in the source directory first
|
554
|
-
source_dir = os.path.dirname(files[0]) if files else None
|
555
|
-
if source_dir:
|
556
|
-
from .media_tags import find_cover_image
|
557
|
-
artwork_path = find_cover_image(source_dir)
|
558
|
-
|
559
|
-
# If no cover in source directory, try to extract it from audio file
|
560
|
-
if not artwork_path and len(files) > 0:
|
561
|
-
from .media_tags import extract_artwork, ensure_mutagen
|
562
|
-
|
563
|
-
# Make sure mutagen is available for artwork extraction
|
564
|
-
if ensure_mutagen(auto_install=args.auto_download):
|
565
|
-
# Try to extract artwork from the first file
|
566
|
-
temp_artwork = extract_artwork(files[0])
|
567
|
-
if temp_artwork:
|
568
|
-
artwork_path = temp_artwork
|
569
|
-
# Note: this creates a temporary file that will be deleted after upload
|
570
|
-
|
571
|
-
# Upload the artwork if found
|
572
|
-
if artwork_path:
|
573
|
-
logger.info("Found artwork: %s", artwork_path)
|
574
|
-
|
575
|
-
# Create artwork upload path - keep same path but use "custom_img" folder
|
576
|
-
artwork_upload_path = args.path
|
577
|
-
if not artwork_upload_path:
|
578
|
-
artwork_upload_path = "/custom_img"
|
579
|
-
elif not artwork_upload_path.startswith("/custom_img"):
|
580
|
-
# Make sure we're using the custom_img folder
|
581
|
-
if artwork_upload_path.startswith("/"):
|
582
|
-
artwork_upload_path = "/custom_img" + artwork_upload_path
|
583
|
-
else:
|
584
|
-
artwork_upload_path = "/custom_img/" + artwork_upload_path
|
585
|
-
|
586
|
-
# Get the original artwork file extension
|
587
|
-
artwork_ext = os.path.splitext(artwork_path)[1]
|
588
|
-
|
589
|
-
# Create a temporary copy with the same name as the taf file
|
590
|
-
import shutil
|
591
|
-
renamed_artwork_path = None
|
592
|
-
try:
|
593
|
-
renamed_artwork_path = os.path.join(os.path.dirname(artwork_path),
|
594
|
-
f"{os.path.splitext(os.path.basename(out_filename))[0]}{artwork_ext}")
|
595
|
-
|
596
|
-
if renamed_artwork_path != artwork_path:
|
597
|
-
shutil.copy2(artwork_path, renamed_artwork_path)
|
598
|
-
logger.debug("Created renamed artwork copy: %s", renamed_artwork_path)
|
599
|
-
|
600
|
-
logger.info("Uploading artwork to path: %s as %s%s",
|
601
|
-
artwork_upload_path, os.path.splitext(os.path.basename(out_filename))[0], artwork_ext)
|
602
|
-
|
603
|
-
artwork_upload_success = upload_to_teddycloud(
|
604
|
-
renamed_artwork_path, teddycloud_url, args.ignore_ssl_verify,
|
605
|
-
args.special_folder, artwork_upload_path, args.show_progress,
|
606
|
-
args.connection_timeout, args.read_timeout,
|
607
|
-
args.max_retries, args.retry_delay
|
608
|
-
)
|
609
|
-
|
610
|
-
if artwork_upload_success:
|
611
|
-
logger.info("Successfully uploaded artwork")
|
612
|
-
else:
|
613
|
-
logger.warning("Failed to upload artwork")
|
614
|
-
|
615
|
-
# Clean up temporary renamed file
|
616
|
-
if renamed_artwork_path != artwork_path and os.path.exists(renamed_artwork_path):
|
617
|
-
try:
|
618
|
-
os.unlink(renamed_artwork_path)
|
619
|
-
logger.debug("Removed temporary renamed artwork file: %s", renamed_artwork_path)
|
620
|
-
except Exception as e:
|
621
|
-
logger.debug("Failed to remove temporary renamed artwork file: %s", e)
|
622
|
-
|
623
|
-
# Clean up temporary extracted artwork file if needed
|
624
|
-
if temp_artwork and os.path.exists(temp_artwork) and temp_artwork != renamed_artwork_path:
|
625
|
-
try:
|
626
|
-
os.unlink(temp_artwork)
|
627
|
-
logger.debug("Removed temporary artwork file: %s", temp_artwork)
|
628
|
-
except Exception as e:
|
629
|
-
logger.debug("Failed to remove temporary artwork file: %s", e)
|
630
|
-
except Exception as e:
|
631
|
-
logger.error("Error during artwork renaming or upload: %s", e)
|
632
|
-
else:
|
633
|
-
logger.warning("No artwork found to upload")
|
634
577
|
|
635
|
-
|
636
|
-
if args.create_custom_json and args.upload:
|
637
|
-
teddycloud_url = args.upload
|
638
|
-
artwork_url = None
|
639
|
-
|
640
|
-
# If artwork was uploaded, construct its URL for the JSON
|
578
|
+
# Handle artwork upload
|
641
579
|
if args.include_artwork:
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
653
|
-
|
654
|
-
|
655
|
-
|
656
|
-
|
657
|
-
# Construct the URL for the artwork based on TeddyCloud structure
|
658
|
-
artwork_path = args.path or "/custom_img"
|
659
|
-
if not artwork_path.endswith('/'):
|
660
|
-
artwork_path += '/'
|
661
|
-
|
662
|
-
artwork_url = f"{teddycloud_url}{artwork_path}{taf_basename}{artwork_ext}"
|
663
|
-
logger.debug("Using artwork URL: %s", artwork_url)
|
664
|
-
|
665
|
-
logger.info("Fetching and updating custom Tonies JSON data")
|
666
|
-
success = fetch_and_update_tonies_json(
|
667
|
-
teddycloud_url,
|
668
|
-
args.ignore_ssl_verify,
|
669
|
-
out_filename,
|
670
|
-
files,
|
671
|
-
artwork_url
|
672
|
-
)
|
673
|
-
|
580
|
+
success, artwork_url = upload_artwork(client, out_filename, source_dir, files)
|
581
|
+
if success:
|
582
|
+
logger.info("Successfully uploaded artwork for %s", out_filename)
|
583
|
+
else:
|
584
|
+
logger.warning("Failed to upload artwork for %s", out_filename)
|
585
|
+
|
586
|
+
if args.create_custom_json:
|
587
|
+
json_output_dir = source_dir if args.output_to_source else './output'
|
588
|
+
client_param = client if 'client' in locals() else None
|
589
|
+
if args.version_2:
|
590
|
+
logger.debug("Using version 2 of the Tonies JSON format")
|
591
|
+
success = fetch_and_update_tonies_json_v2(client_param, out_filename, files, artwork_url, json_output_dir)
|
592
|
+
else:
|
593
|
+
success = fetch_and_update_tonies_json_v1(client_param, out_filename, files, artwork_url, json_output_dir)
|
674
594
|
if success:
|
675
|
-
logger.info("Successfully updated
|
595
|
+
logger.info("Successfully updated Tonies JSON for %s", out_filename)
|
676
596
|
else:
|
677
|
-
logger.warning("Failed to update
|
597
|
+
logger.warning("Failed to update Tonies JSON for %s", out_filename)
|
678
598
|
|
679
599
|
if __name__ == "__main__":
|
680
600
|
main()
|