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