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/tonies_json.py
CHANGED
@@ -8,50 +8,46 @@ which can be used to manage custom Tonies on TeddyCloud servers.
|
|
8
8
|
import os
|
9
9
|
import json
|
10
10
|
import time
|
11
|
-
import urllib.error
|
12
|
-
import ssl
|
13
|
-
import uuid
|
14
11
|
import locale
|
15
12
|
import re
|
16
|
-
|
13
|
+
import hashlib
|
14
|
+
import mutagen
|
15
|
+
from typing import Dict, Any, List, Optional
|
17
16
|
|
18
17
|
from .logger import get_logger
|
19
18
|
from .media_tags import get_file_tags, extract_album_info
|
20
19
|
from .constants import LANGUAGE_MAPPING, GENRE_MAPPING
|
21
|
-
from .teddycloud import
|
20
|
+
from .teddycloud import TeddyCloudClient
|
22
21
|
|
23
22
|
logger = get_logger('tonies_json')
|
24
23
|
|
25
24
|
class ToniesJsonHandler:
|
26
25
|
"""Handler for tonies.custom.json operations."""
|
27
26
|
|
28
|
-
def __init__(self,
|
27
|
+
def __init__(self, client: TeddyCloudClient = None):
|
29
28
|
"""
|
30
29
|
Initialize the handler.
|
31
30
|
|
32
31
|
Args:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
self.teddycloud_url = teddycloud_url.rstrip('/') if teddycloud_url else None
|
37
|
-
self.ignore_ssl_verify = ignore_ssl_verify
|
32
|
+
client: TeddyCloudClient instance to use for API communication
|
33
|
+
"""
|
34
|
+
self.client = client
|
38
35
|
self.custom_json = []
|
39
36
|
self.is_loaded = False
|
40
|
-
|
37
|
+
|
41
38
|
def load_from_server(self) -> bool:
|
42
39
|
"""
|
43
40
|
Load tonies.custom.json from the TeddyCloud server.
|
44
41
|
|
45
42
|
Returns:
|
46
43
|
True if successful, False otherwise
|
47
|
-
"""
|
48
|
-
if
|
49
|
-
logger.error("Cannot load from server:
|
44
|
+
"""
|
45
|
+
if self.client is None:
|
46
|
+
logger.error("Cannot load from server: no client provided")
|
50
47
|
return False
|
51
48
|
|
52
49
|
try:
|
53
|
-
result =
|
54
|
-
|
50
|
+
result = self.client.get_tonies_custom_json()
|
55
51
|
if result is not None:
|
56
52
|
self.custom_json = result
|
57
53
|
self.is_loaded = True
|
@@ -98,39 +94,6 @@ class ToniesJsonHandler:
|
|
98
94
|
logger.error("Error loading tonies.custom.json from file: %s", e)
|
99
95
|
return False
|
100
96
|
|
101
|
-
def save_to_server(self) -> bool:
|
102
|
-
"""
|
103
|
-
Save tonies.custom.json to the TeddyCloud server.
|
104
|
-
|
105
|
-
Returns:
|
106
|
-
True if successful, False otherwise
|
107
|
-
"""
|
108
|
-
if not self.teddycloud_url:
|
109
|
-
logger.error("Cannot save to server: No TeddyCloud URL provided")
|
110
|
-
return False
|
111
|
-
|
112
|
-
if not self.is_loaded:
|
113
|
-
logger.error("Cannot save tonies.custom.json: data not loaded")
|
114
|
-
return False
|
115
|
-
|
116
|
-
try:
|
117
|
-
result = put_tonies_custom_json_to_server(
|
118
|
-
self.teddycloud_url,
|
119
|
-
self.custom_json,
|
120
|
-
self.ignore_ssl_verify
|
121
|
-
)
|
122
|
-
|
123
|
-
if result:
|
124
|
-
logger.info("Successfully saved tonies.custom.json to server")
|
125
|
-
return True
|
126
|
-
else:
|
127
|
-
logger.error("Failed to save tonies.custom.json to server")
|
128
|
-
return False
|
129
|
-
|
130
|
-
except Exception as e:
|
131
|
-
logger.error("Error saving tonies.custom.json to server: %s", e)
|
132
|
-
return False
|
133
|
-
|
134
97
|
def save_to_file(self, file_path: str) -> bool:
|
135
98
|
"""
|
136
99
|
Save tonies.custom.json to a local file.
|
@@ -146,9 +109,7 @@ class ToniesJsonHandler:
|
|
146
109
|
return False
|
147
110
|
|
148
111
|
try:
|
149
|
-
# Ensure the directory exists
|
150
112
|
os.makedirs(os.path.dirname(os.path.abspath(file_path)), exist_ok=True)
|
151
|
-
|
152
113
|
logger.info("Saving tonies.custom.json to file: %s", file_path)
|
153
114
|
with open(file_path, 'w', encoding='utf-8') as f:
|
154
115
|
json.dump(self.custom_json, f, indent=2, ensure_ascii=False)
|
@@ -163,6 +124,8 @@ class ToniesJsonHandler:
|
|
163
124
|
def add_entry_from_taf(self, taf_file: str, input_files: List[str], artwork_url: Optional[str] = None) -> bool:
|
164
125
|
"""
|
165
126
|
Add an entry to the custom JSON from a TAF file.
|
127
|
+
If an entry with the same hash exists, it will be updated.
|
128
|
+
If an entry with the same series+episode exists, the new hash will be added to it.
|
166
129
|
|
167
130
|
Args:
|
168
131
|
taf_file: Path to the TAF file
|
@@ -182,17 +145,77 @@ class ToniesJsonHandler:
|
|
182
145
|
try:
|
183
146
|
logger.info("Adding entry for %s to tonies.custom.json", taf_file)
|
184
147
|
|
185
|
-
logger.debug("Generating article ID")
|
186
|
-
article_id = self._generate_article_id()
|
187
|
-
logger.debug("Generated article ID: %s", article_id)
|
188
|
-
|
189
148
|
logger.debug("Extracting metadata from input files")
|
190
149
|
metadata = self._extract_metadata_from_files(input_files)
|
191
150
|
logger.debug("Extracted metadata: %s", metadata)
|
151
|
+
with open(taf_file, 'rb') as f:
|
152
|
+
taf_hash = hashlib.sha1(f.read()).hexdigest()
|
153
|
+
|
154
|
+
taf_size = os.path.getsize(taf_file)
|
155
|
+
timestamp = int(time.time())
|
156
|
+
series = metadata.get('albumartist', metadata.get('artist', 'Unknown Artist'))
|
157
|
+
episode = metadata.get('album', os.path.splitext(os.path.basename(taf_file))[0])
|
158
|
+
track_desc = metadata.get('track_descriptions', [])
|
159
|
+
language = self._determine_language(metadata)
|
160
|
+
category = self._determine_category(metadata)
|
161
|
+
age = self._estimate_age(metadata)
|
162
|
+
new_id_entry = {
|
163
|
+
"audio-id": timestamp,
|
164
|
+
"hash": taf_hash,
|
165
|
+
"size": taf_size,
|
166
|
+
"tracks": len(track_desc),
|
167
|
+
"confidence": 1
|
168
|
+
}
|
169
|
+
existing_entry, entry_idx, data_idx = self.find_entry_by_hash(taf_hash)
|
170
|
+
if existing_entry:
|
171
|
+
logger.info("Found existing entry with the same hash, updating it")
|
172
|
+
data = existing_entry['data'][data_idx]
|
173
|
+
if artwork_url and artwork_url != data.get('image', ''):
|
174
|
+
logger.debug("Updating artwork URL")
|
175
|
+
data['image'] = artwork_url
|
176
|
+
if track_desc and track_desc != data.get('track-desc', []):
|
177
|
+
logger.debug("Updating track descriptions")
|
178
|
+
data['track-desc'] = track_desc
|
179
|
+
|
180
|
+
logger.info("Successfully updated existing entry for %s", taf_file)
|
181
|
+
return True
|
182
|
+
existing_entry, entry_idx, data_idx = self.find_entry_by_series_episode(series, episode)
|
183
|
+
if existing_entry:
|
184
|
+
logger.info("Found existing entry with the same series/episode, adding hash to it")
|
185
|
+
existing_data = existing_entry['data'][data_idx]
|
186
|
+
if 'ids' not in existing_data:
|
187
|
+
existing_data['ids'] = []
|
188
|
+
|
189
|
+
existing_data['ids'].append(new_id_entry)
|
190
|
+
if artwork_url and artwork_url != existing_data.get('image', ''):
|
191
|
+
logger.debug("Updating artwork URL")
|
192
|
+
existing_data['image'] = artwork_url
|
193
|
+
|
194
|
+
logger.info("Successfully added new hash to existing entry for %s", taf_file)
|
195
|
+
return True
|
196
|
+
logger.debug("No existing entry found, creating new entry")
|
197
|
+
logger.debug("Generating article ID")
|
198
|
+
article_id = self._generate_article_id()
|
199
|
+
logger.debug("Generated article ID: %s", article_id)
|
192
200
|
|
193
|
-
|
194
|
-
|
195
|
-
|
201
|
+
entry = {
|
202
|
+
"article": article_id,
|
203
|
+
"data": [
|
204
|
+
{
|
205
|
+
"series": series,
|
206
|
+
"episode": episode,
|
207
|
+
"release": timestamp,
|
208
|
+
"language": language,
|
209
|
+
"category": category,
|
210
|
+
"runtime": self._calculate_runtime(input_files),
|
211
|
+
"age": age,
|
212
|
+
"origin": "custom",
|
213
|
+
"image": artwork_url if artwork_url else "",
|
214
|
+
"track-desc": track_desc,
|
215
|
+
"ids": [new_id_entry]
|
216
|
+
}
|
217
|
+
]
|
218
|
+
}
|
196
219
|
|
197
220
|
self.custom_json.append(entry)
|
198
221
|
logger.debug("Added entry to custom_json (new length: %d)", len(self.custom_json))
|
@@ -214,8 +237,6 @@ class ToniesJsonHandler:
|
|
214
237
|
Unique article ID in the format "tt-42" followed by sequential number starting from 0
|
215
238
|
"""
|
216
239
|
logger.trace("Entering _generate_article_id()")
|
217
|
-
|
218
|
-
# Find the highest sequential number for tt-42 IDs
|
219
240
|
highest_num = -1
|
220
241
|
pattern = re.compile(r'tt-42(\d+)')
|
221
242
|
|
@@ -234,11 +255,7 @@ class ToniesJsonHandler:
|
|
234
255
|
pass
|
235
256
|
|
236
257
|
logger.debug("Highest tt-42 ID number found: %d", highest_num)
|
237
|
-
|
238
|
-
# Generate the next sequential number
|
239
258
|
next_num = highest_num + 1
|
240
|
-
|
241
|
-
# Format the ID with leading zeros to make it 10 digits
|
242
259
|
result = f"tt-42{next_num:010d}"
|
243
260
|
logger.debug("Generated new article ID: %s", result)
|
244
261
|
|
@@ -246,26 +263,25 @@ class ToniesJsonHandler:
|
|
246
263
|
return result
|
247
264
|
|
248
265
|
def _extract_metadata_from_files(self, input_files: List[str]) -> Dict[str, Any]:
|
249
|
-
|
250
|
-
|
251
|
-
# If there are multiple files in the same folder, use album info
|
252
|
-
if len(input_files) > 1 and os.path.dirname(input_files[0]) == os.path.dirname(input_files[-1]):
|
253
|
-
folder_path = os.path.dirname(input_files[0])
|
254
|
-
album_info = extract_album_info(folder_path)
|
255
|
-
metadata.update(album_info)
|
266
|
+
"""
|
267
|
+
Extract metadata from audio files to use in the custom JSON entry.
|
256
268
|
|
257
|
-
|
269
|
+
Args:
|
270
|
+
input_files: List of paths to audio files
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
Dictionary containing metadata extracted from files
|
274
|
+
"""
|
275
|
+
metadata = {}
|
258
276
|
track_descriptions = []
|
259
277
|
for file_path in input_files:
|
260
278
|
tags = get_file_tags(file_path)
|
261
279
|
if 'title' in tags:
|
262
280
|
track_descriptions.append(tags['title'])
|
263
281
|
else:
|
264
|
-
# Use filename as fallback
|
265
282
|
filename = os.path.splitext(os.path.basename(file_path))[0]
|
266
283
|
track_descriptions.append(filename)
|
267
284
|
|
268
|
-
# Extract language and genre from the first file if not already present
|
269
285
|
if 'language' not in metadata and 'language' in tags:
|
270
286
|
metadata['language'] = tags['language']
|
271
287
|
|
@@ -277,51 +293,31 @@ class ToniesJsonHandler:
|
|
277
293
|
return metadata
|
278
294
|
|
279
295
|
def _determine_language(self, metadata: Dict[str, Any]) -> str:
|
280
|
-
# Check for language tag in metadata
|
281
296
|
if 'language' in metadata:
|
282
297
|
lang_value = metadata['language'].lower().strip()
|
283
298
|
if lang_value in LANGUAGE_MAPPING:
|
284
299
|
return LANGUAGE_MAPPING[lang_value]
|
285
|
-
|
286
|
-
# If not found, try to use system locale
|
287
300
|
try:
|
288
301
|
system_lang, _ = locale.getdefaultlocale()
|
289
302
|
if system_lang:
|
290
303
|
lang_code = system_lang.split('_')[0].lower()
|
291
304
|
if lang_code in LANGUAGE_MAPPING:
|
292
305
|
return LANGUAGE_MAPPING[lang_code]
|
293
|
-
# Try to map system language code to tonie format
|
294
|
-
if lang_code == 'de':
|
295
|
-
return 'de-de'
|
296
|
-
elif lang_code == 'en':
|
297
|
-
return 'en-us'
|
298
|
-
elif lang_code == 'fr':
|
299
|
-
return 'fr-fr'
|
300
|
-
elif lang_code == 'it':
|
301
|
-
return 'it-it'
|
302
|
-
elif lang_code == 'es':
|
303
|
-
return 'es-es'
|
304
306
|
except Exception:
|
305
307
|
pass
|
306
|
-
|
307
|
-
# Default to German as it's most common for Tonies
|
308
308
|
return 'de-de'
|
309
309
|
|
310
310
|
def _determine_category(self, metadata: Dict[str, Any]) -> str:
|
311
|
-
# Check for genre tag in metadata
|
312
311
|
if 'genre' in metadata:
|
313
312
|
genre_value = metadata['genre'].lower().strip()
|
314
313
|
|
315
|
-
# Check for direct mapping
|
316
314
|
if genre_value in GENRE_MAPPING:
|
317
315
|
return GENRE_MAPPING[genre_value]
|
318
316
|
|
319
|
-
# Check for partial matching
|
320
317
|
for genre_key, category in GENRE_MAPPING.items():
|
321
318
|
if genre_key in genre_value:
|
322
319
|
return category
|
323
|
-
|
324
|
-
# Check for common keywords in the genre
|
320
|
+
|
325
321
|
if any(keyword in genre_value for keyword in ['musik', 'song', 'music', 'lied']):
|
326
322
|
return 'music'
|
327
323
|
elif any(keyword in genre_value for keyword in ['hörspiel', 'hörspiele', 'audio play']):
|
@@ -334,8 +330,6 @@ class ToniesJsonHandler:
|
|
334
330
|
return 'Wissen & Hörmagazine'
|
335
331
|
elif any(keyword in genre_value for keyword in ['schlaf', 'sleep', 'meditation']):
|
336
332
|
return 'Schlaflieder & Entspannung'
|
337
|
-
|
338
|
-
# Default to standard category for most custom content
|
339
333
|
return 'Hörspiele & Hörbücher'
|
340
334
|
|
341
335
|
def _estimate_age(self, metadata: Dict[str, Any]) -> int:
|
@@ -363,67 +357,136 @@ class ToniesJsonHandler:
|
|
363
357
|
|
364
358
|
return default_age
|
365
359
|
|
366
|
-
def
|
367
|
-
|
368
|
-
|
369
|
-
|
360
|
+
def find_entry_by_hash(self, taf_hash: str) -> tuple[Optional[Dict[str, Any]], Optional[int], Optional[int]]:
|
361
|
+
"""
|
362
|
+
Find an entry in the custom JSON by TAF hash.
|
363
|
+
|
364
|
+
Args:
|
365
|
+
taf_hash: SHA1 hash of the TAF file to find
|
366
|
+
|
367
|
+
Returns:
|
368
|
+
Tuple of (entry, entry_index, data_index) if found, or (None, None, None) if not found
|
369
|
+
"""
|
370
|
+
logger.trace("Searching for entry with hash %s", taf_hash)
|
371
|
+
|
372
|
+
for entry_idx, entry in enumerate(self.custom_json):
|
373
|
+
if 'data' not in entry:
|
374
|
+
continue
|
375
|
+
|
376
|
+
for data_idx, data in enumerate(entry['data']):
|
377
|
+
if 'ids' not in data:
|
378
|
+
continue
|
379
|
+
|
380
|
+
for id_entry in data['ids']:
|
381
|
+
if id_entry.get('hash') == taf_hash:
|
382
|
+
logger.debug("Found existing entry with matching hash %s", taf_hash)
|
383
|
+
return entry, entry_idx, data_idx
|
370
384
|
|
371
|
-
|
372
|
-
|
385
|
+
logger.debug("No entry found with hash %s", taf_hash)
|
386
|
+
return None, None, None
|
387
|
+
|
388
|
+
def find_entry_by_series_episode(self, series: str, episode: str) -> tuple[Optional[Dict[str, Any]], Optional[int], Optional[int]]:
|
389
|
+
"""
|
390
|
+
Find an entry in the custom JSON by series and episode.
|
373
391
|
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
392
|
+
Args:
|
393
|
+
series: Series name to find
|
394
|
+
episode: Episode name to find
|
395
|
+
|
396
|
+
Returns:
|
397
|
+
Tuple of (entry, entry_index, data_index) if found, or (None, None, None) if not found
|
398
|
+
"""
|
399
|
+
logger.trace("Searching for entry with series='%s', episode='%s'", series, episode)
|
381
400
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
401
|
+
for entry_idx, entry in enumerate(self.custom_json):
|
402
|
+
if 'data' not in entry:
|
403
|
+
continue
|
404
|
+
|
405
|
+
for data_idx, data in enumerate(entry['data']):
|
406
|
+
if data.get('series') == series and data.get('episode') == episode:
|
407
|
+
logger.debug("Found existing entry with matching series/episode: %s / %s", series, episode)
|
408
|
+
return entry, entry_idx, data_idx
|
386
409
|
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
{
|
404
|
-
"audio-id": timestamp,
|
405
|
-
"hash": taf_hash,
|
406
|
-
"size": taf_size,
|
407
|
-
"tracks": len(track_desc),
|
408
|
-
"confidence": 1
|
409
|
-
}
|
410
|
-
]
|
411
|
-
}
|
412
|
-
]
|
413
|
-
}
|
410
|
+
logger.debug("No entry found with series/episode: %s / %s", series, episode)
|
411
|
+
return None, None, None
|
412
|
+
|
413
|
+
def _calculate_runtime(self, input_files: List[str]) -> int:
|
414
|
+
"""
|
415
|
+
Calculate the total runtime in minutes from a list of audio files.
|
416
|
+
|
417
|
+
Args:
|
418
|
+
input_files: List of paths to audio files
|
419
|
+
|
420
|
+
Returns:
|
421
|
+
Total runtime in minutes (rounded to the nearest minute)
|
422
|
+
"""
|
423
|
+
logger.trace("Entering _calculate_runtime() with %d input files", len(input_files))
|
424
|
+
total_runtime_seconds = 0
|
425
|
+
processed_files = 0
|
414
426
|
|
415
|
-
|
427
|
+
try:
|
428
|
+
logger.debug("Starting runtime calculation for %d audio files", len(input_files))
|
429
|
+
|
430
|
+
for i, file_path in enumerate(input_files):
|
431
|
+
logger.trace("Processing file %d/%d: %s", i+1, len(input_files), file_path)
|
432
|
+
|
433
|
+
if not os.path.exists(file_path):
|
434
|
+
logger.warning("File does not exist: %s", file_path)
|
435
|
+
continue
|
436
|
+
|
437
|
+
try:
|
438
|
+
logger.trace("Loading audio file with mutagen: %s", file_path)
|
439
|
+
audio = mutagen.File(file_path)
|
440
|
+
|
441
|
+
if audio is None:
|
442
|
+
logger.warning("Mutagen could not identify file format: %s", file_path)
|
443
|
+
continue
|
444
|
+
|
445
|
+
if not hasattr(audio, 'info'):
|
446
|
+
logger.warning("Audio file has no info attribute: %s", file_path)
|
447
|
+
continue
|
448
|
+
|
449
|
+
if not hasattr(audio.info, 'length'):
|
450
|
+
logger.warning("Audio info has no length attribute: %s", file_path)
|
451
|
+
continue
|
452
|
+
|
453
|
+
file_runtime_seconds = int(audio.info.length)
|
454
|
+
total_runtime_seconds += file_runtime_seconds
|
455
|
+
processed_files += 1
|
456
|
+
|
457
|
+
logger.debug("File %s: runtime=%d seconds, format=%s",
|
458
|
+
file_path, file_runtime_seconds, audio.__class__.__name__)
|
459
|
+
logger.trace("Current total runtime: %d seconds after %d/%d files",
|
460
|
+
total_runtime_seconds, i+1, len(input_files))
|
461
|
+
|
462
|
+
except Exception as e:
|
463
|
+
logger.warning("Error processing file %s: %s", file_path, e)
|
464
|
+
logger.trace("Exception details for %s: %s", file_path, str(e), exc_info=True)
|
416
465
|
|
466
|
+
# Convert seconds to minutes, rounding to nearest minute
|
467
|
+
total_runtime_minutes = round(total_runtime_seconds / 60)
|
468
|
+
|
469
|
+
logger.info("Calculated total runtime: %d seconds (%d minutes) from %d/%d files",
|
470
|
+
total_runtime_seconds, total_runtime_minutes, processed_files, len(input_files))
|
471
|
+
|
472
|
+
except ImportError as e:
|
473
|
+
logger.warning("Mutagen library not available, cannot calculate runtime: %s", str(e))
|
474
|
+
return 0
|
475
|
+
except Exception as e:
|
476
|
+
logger.error("Unexpected error during runtime calculation: %s", str(e))
|
477
|
+
logger.trace("Exception details: %s", str(e), exc_info=True)
|
478
|
+
return 0
|
417
479
|
|
418
|
-
|
419
|
-
|
480
|
+
logger.trace("Exiting _calculate_runtime() with total runtime=%d minutes", total_runtime_minutes)
|
481
|
+
return total_runtime_minutes
|
482
|
+
|
483
|
+
def fetch_and_update_tonies_json(client: TeddyCloudClient, taf_file: Optional[str] = None, input_files: Optional[List[str]] = None,
|
420
484
|
artwork_url: Optional[str] = None, output_dir: Optional[str] = None) -> bool:
|
421
485
|
"""
|
422
486
|
Fetch tonies.custom.json from server and merge with local file if it exists, then update with new entry.
|
423
487
|
|
424
488
|
Args:
|
425
|
-
|
426
|
-
ignore_ssl_verify: If True, SSL certificate verification will be disabled
|
489
|
+
client: TeddyCloudClient instance to use for API communication
|
427
490
|
taf_file: Path to the TAF file to add
|
428
491
|
input_files: List of input audio files used to create the TAF
|
429
492
|
artwork_url: URL of the uploaded artwork (if any)
|
@@ -432,71 +495,85 @@ def fetch_and_update_tonies_json(teddycloud_url: Optional[str] = None, ignore_ss
|
|
432
495
|
Returns:
|
433
496
|
True if successful, False otherwise
|
434
497
|
"""
|
435
|
-
|
498
|
+
logger.trace("Entering fetch_and_update_tonies_json with client=%s, taf_file=%s, input_files=%s, artwork_url=%s, output_dir=%s",
|
499
|
+
client, taf_file, input_files, artwork_url, output_dir)
|
436
500
|
|
437
|
-
|
501
|
+
handler = ToniesJsonHandler(client)
|
438
502
|
if not output_dir:
|
439
503
|
output_dir = './output'
|
440
|
-
|
441
|
-
|
504
|
+
logger.debug("No output directory specified, using default: %s", output_dir)
|
505
|
+
|
442
506
|
os.makedirs(output_dir, exist_ok=True)
|
507
|
+
logger.debug("Ensuring output directory exists: %s", output_dir)
|
443
508
|
|
444
|
-
# Create the full path for the JSON file
|
445
509
|
json_file_path = os.path.join(output_dir, 'tonies.custom.json')
|
510
|
+
logger.debug("JSON file path: %s", json_file_path)
|
446
511
|
|
447
512
|
loaded_from_server = False
|
448
|
-
|
449
|
-
# Step 1: Try to get live version from the server first
|
450
|
-
if teddycloud_url:
|
513
|
+
if client:
|
451
514
|
logger.info("Attempting to load tonies.custom.json from server")
|
452
515
|
loaded_from_server = handler.load_from_server()
|
516
|
+
logger.debug("Load from server result: %s", "success" if loaded_from_server else "failed")
|
517
|
+
else:
|
518
|
+
logger.debug("No client provided, skipping server load")
|
453
519
|
|
454
|
-
# Step 2: If we have a local file, merge with the server content
|
455
520
|
if os.path.exists(json_file_path):
|
456
521
|
logger.info("Local tonies.custom.json file found, merging with server content")
|
522
|
+
logger.debug("Local file exists at %s, size: %d bytes", json_file_path, os.path.getsize(json_file_path))
|
457
523
|
|
458
|
-
# Create a temporary handler to load local content
|
459
524
|
local_handler = ToniesJsonHandler()
|
460
525
|
if local_handler.load_from_file(json_file_path):
|
526
|
+
logger.debug("Successfully loaded local file with %d entries", len(local_handler.custom_json))
|
527
|
+
|
461
528
|
if loaded_from_server:
|
462
|
-
|
463
|
-
# Use server-loaded content as base, then add any local entries not in server version
|
529
|
+
logger.debug("Merging local entries with server entries")
|
464
530
|
server_article_ids = {entry.get('article') for entry in handler.custom_json}
|
531
|
+
logger.debug("Found %d unique article IDs from server", len(server_article_ids))
|
532
|
+
|
533
|
+
added_count = 0
|
465
534
|
for local_entry in local_handler.custom_json:
|
466
535
|
local_article_id = local_entry.get('article')
|
467
536
|
if local_article_id not in server_article_ids:
|
468
|
-
logger.
|
537
|
+
logger.trace("Adding local-only entry %s to merged content", local_article_id)
|
469
538
|
handler.custom_json.append(local_entry)
|
539
|
+
added_count += 1
|
540
|
+
|
541
|
+
logger.debug("Added %d local-only entries to merged content", added_count)
|
470
542
|
else:
|
471
|
-
|
543
|
+
logger.debug("Using only local entries (server load failed or no client)")
|
472
544
|
handler.custom_json = local_handler.custom_json
|
473
545
|
handler.is_loaded = True
|
474
546
|
logger.info("Using local tonies.custom.json content")
|
475
547
|
elif not loaded_from_server:
|
476
|
-
|
548
|
+
logger.debug("No local file found and server load failed, starting with empty list")
|
477
549
|
handler.custom_json = []
|
478
550
|
handler.is_loaded = True
|
479
551
|
logger.info("No tonies.custom.json found, starting with empty list")
|
480
552
|
|
481
|
-
# Add entry if needed
|
482
553
|
if taf_file and input_files and handler.is_loaded:
|
554
|
+
logger.debug("Adding new entry for TAF file: %s", taf_file)
|
555
|
+
logger.debug("Using %d input files for metadata extraction", len(input_files))
|
556
|
+
|
483
557
|
if not handler.add_entry_from_taf(taf_file, input_files, artwork_url):
|
484
558
|
logger.error("Failed to add entry to tonies.custom.json")
|
559
|
+
logger.trace("Exiting fetch_and_update_tonies_json with success=False (failed to add entry)")
|
485
560
|
return False
|
561
|
+
|
562
|
+
logger.debug("Successfully added new entry for %s", taf_file)
|
563
|
+
else:
|
564
|
+
if not taf_file:
|
565
|
+
logger.debug("No TAF file provided, skipping add entry step")
|
566
|
+
elif not input_files:
|
567
|
+
logger.debug("No input files provided, skipping add entry step")
|
568
|
+
elif not handler.is_loaded:
|
569
|
+
logger.debug("Handler not properly loaded, skipping add entry step")
|
486
570
|
|
487
|
-
|
571
|
+
logger.debug("Saving updated tonies.custom.json to %s", json_file_path)
|
488
572
|
if not handler.save_to_file(json_file_path):
|
489
573
|
logger.error("Failed to save tonies.custom.json to file")
|
574
|
+
logger.trace("Exiting fetch_and_update_tonies_json with success=False (failed to save file)")
|
490
575
|
return False
|
491
576
|
|
492
|
-
|
493
|
-
|
494
|
-
#if teddycloud_url and handler.is_loaded:
|
495
|
-
try:
|
496
|
-
if not handler.save_to_server():
|
497
|
-
logger.warning("Could not save tonies.custom.json to server")
|
498
|
-
except Exception as e:
|
499
|
-
logger.warning("Error when saving tonies.custom.json to server: %s", e)
|
500
|
-
# Don't fail the operation if server upload fails
|
501
|
-
|
577
|
+
logger.debug("Successfully saved tonies.custom.json with %d entries", len(handler.custom_json))
|
578
|
+
logger.trace("Exiting fetch_and_update_tonies_json with success=True")
|
502
579
|
return True
|