From fb3494ffbf92eddb0d058df1b0c2ff669ee5b116 Mon Sep 17 00:00:00 2001 From: weatherhog Date: Mon, 26 Oct 2026 14:11:34 +0100 Subject: [PATCH 1/2] feat: show corrupted TAFs, check and redownload them from the cloud A warning icon on the Tonie card marks a corrupted TAF. The Tonie information modal can run the full SHA1/length check on demand and redownload the content from the cloud. A `corrupt` field is added to the custom filter. Needs the server side from toniebox-reverse-engineering/teddycloud for `corrupt`, `redownloadUrl` and `getTagInfo?validate=true`. Part of toniebox-reverse-engineering/teddycloud#338 --- CHANGELOG.md | 3 + public/translations/de.json | 23 ++++- public/translations/en.json | 23 ++++- public/translations/es.json | 23 ++++- public/translations/fr.json | 23 ++++- public/translations/it.json | 23 ++++- public/translations/pl.json | 23 ++++- src/api/apis/TeddyCloudApi.ts | 8 +- .../common/modals/TonieInformationModal.tsx | 66 +++++++++++--- src/components/tonies/toniecard/TonieCard.tsx | 47 +++++++++- .../toniecard/hooks/useTonieCardActions.ts | 89 +++++++++++++++---- .../tonieslist/hooks/useToniesFilter.test.ts | 13 +++ .../tonieslist/hooks/useToniesFilter.ts | 17 +++- .../modals/ToniesCustomFilterHelpModal.tsx | 4 + src/types/tonieTypes.ts | 2 + 15 files changed, 335 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab40f56e..86a125e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ - Fixed Docker startup when the configured PUID/PGID already belongs to an existing account in the base image - Greatly expanded automated backend testing and CI, including isolated test environments, C and pytest test suites, simulated Tonieboxes with hardware-specific profiles, a fake Boxine cloud and additional security regression tests [https://github.com/toniebox-reverse-engineering/teddycloud/pull/461](https://github.com/toniebox-reverse-engineering/teddycloud/pull/461) - Added and expanded architecture, settings, streaming and development documentation +- Fixed a corrupted TAF in the library being kept when the same content was cached from the cloud again [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) +- Added `corrupt` and `redownloadUrl` to the tag index, a forced cloud download (`force=true`) and a full TAF check on demand (`getTagInfo?validate=true`) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) - gui: Download TAF tracks as separate .ogg files (ZIP when more than one track is selected) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/169](https://github.com/toniebox-reverse-engineering/teddycloud/issues/169) - gui: Box setup can create a port patch for CC3200 boxes (altPort.custom.json) and patch the port of ESP32 boxes, e.g. for a proxy that routes boxes by port - gui: Certificate check per box (issued by TeddyCloud / pinned / other certificate) on the box card and in the certificates dialog [https://github.com/toniebox-reverse-engineering/teddycloud/issues/85](https://github.com/toniebox-reverse-engineering/teddycloud/issues/85) @@ -56,6 +58,7 @@ - gui: Fixed TAF track downloads in the TonieInformationModal to use the correct linked/source TAF for Content and Tonies views, including uncached Tonies, and keep track selections across re-renders, screen size changes and audio player updates - gui: TeddyStudio: Reworked label printing to generate a browser-independent PDF instead of relying on CSS print layouts, including PDF preview and download functionality [https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/286](https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/286) - gui: Adapted outdated hint to new release candidate tags +- gui: Show corrupted TAF files on the Tonie card, check a TAF on demand and redownload it from the cloud in the Tonie information [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) ### Commits diff --git a/public/translations/de.json b/public/translations/de.json index 5241aad5..c7ec1542 100644 --- a/public/translations/de.json +++ b/public/translations/de.json @@ -1978,6 +1978,7 @@ "title": "Content", "toniePicture": "Tonie Bild" }, + "corruptHint": "Die Audiodatei ist beschädigt. Öffne die Informationen, um sie erneut aus der Cloud herunterzuladen.", "currentPath": "Aktueller Pfad: ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Bild auswählen" }, "infoModal": { + "corrupt": "Beschädigt:", "download": "Download TAF als *.ogg", "exists": "Existiert:", "model": "Modell:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Tonie/Tag ausblenden", "ok": "OK", - "unknownModel": "Unbekanntes Model" + "redownload": "Erneut herunterladen", + "redownloadConfirm": "Die lokale Datei durch einen neuen Download aus der Cloud ersetzen?", + "unknownModel": "Unbekanntes Model", + "verifyFile": "Datei prüfen", + "verifyFileHint": "Liest die ganze Audiodatei und prüft SHA1-Hash und Länge. Kann eine Weile dauern." }, "lastPlayedOnModal": { "lastPlayedOn": "Zuletzt gepielter Tonie", @@ -2337,6 +2343,10 @@ "downloadingDetails": "\"{{model}}\" [{{ruid}}] wird heruntergeladen...", "errorDuringDownload": "Fehler beim Herunterladen im Hintergrund", "errorDuringDownloadDetails": "Fehler beim Hintergrund-Download von \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Fehler beim erneuten Herunterladen", + "errorDuringRedownloadDetails": "Fehler beim erneuten Herunterladen von \"{{model}}\" [{{ruid}}]: ", + "errorDuringVerify": "Fehler bei der Prüfung", + "errorDuringVerifyDetails": "Fehler bei der Prüfung von \"{{model}}\" [{{ruid}}]: ", "errorFetchingUpdatedCard": "Fehler beim Abrufen der aktualisierten Karte!", "errorFetchingUpdatedCardDetails": "Fehler beim Abrufen der aktualisierten Karte für \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filter gelöscht", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Live-Flag für \"{{model}}\" [{{ruid}}] aktiviert", "noFilterFound": "Kein Filter gefunden", "noFilterFoundDetails": "Kein Filter mit dem Namen {{name}} wurde gefunden", + "redownloadedFile": "Datei erneut heruntergeladen", + "redownloadedFileDetails": "\"{{model}}\" [{{ruid}}] wurde erneut aus der Cloud heruntergeladen", + "redownloading": "Lade erneut herunter...", + "redownloadingDetails": "Lade \"{{model}}\" [{{ruid}}] erneut aus der Cloud herunter...", "setToEmptyValue": "leer", "setTonieToModelFailed": "Konnte Tonie-Modell nicht ändern!", "setTonieToModelFailedDetails": "Konnte Tonie-Modell für [{{ruid}}] nicht ändern: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "Konnte Tonie-Quelle nicht ändern!", "setTonieToSourceFailedDetails": "Konnte Tonie-Quelle für [{{ruid}}] nicht ändern: ", "setTonieToSourceSuccessful": "Tonie-Quelle erfolgreich gesetzt!", - "setTonieToSourceSuccessfulDetails": "Tonie-Quelle auf {{selectedSource}} für [{{ruid}}] gesetzt" + "setTonieToSourceSuccessfulDetails": "Tonie-Quelle auf {{selectedSource}} für [{{ruid}}] gesetzt", + "verifiedCorrupt": "Datei ist beschädigt", + "verifiedCorruptDetails": "Die Audiodatei von \"{{model}}\" [{{ruid}}] hat die vollständige Prüfung nicht bestanden. Lade sie erneut aus der Cloud herunter.", + "verifiedValid": "Datei ist gültig", + "verifiedValidDetails": "Die Audiodatei von \"{{model}}\" [{{ruid}}] hat die vollständige Prüfung bestanden" }, "navigationTitle": "Tonies", "noData": "Keine Tonies/benutzerdefinierten Tags gefunden.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Ob dieser Tonie beansprucht wurde (true/false).", + "corrupt": "Ob die Audiodatei die vollständige Prüfung nicht bestanden hat (true/false).", "episode": "Der Name der Episode des Tonies.", "exists": "Ob der Tonie existiert (true/false).", "hasCloudAuth": "Ob der Tonie Cloud-Zugriff hat (true/false).", diff --git a/public/translations/en.json b/public/translations/en.json index 2878a250..adbe2f41 100644 --- a/public/translations/en.json +++ b/public/translations/en.json @@ -1978,6 +1978,7 @@ "title": "Content", "toniePicture": "Tonie Picture" }, + "corruptHint": "The audio file is corrupted. Open the information to redownload it from the cloud.", "currentPath": "Current Path: ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Select image" }, "infoModal": { + "corrupt": "Corrupted:", "download": "Download TAF as *.ogg", "exists": "Exists:", "model": "Model:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Hide Tonie/Tag", "ok": "OK", - "unknownModel": "Unknown Model" + "redownload": "Redownload", + "redownloadConfirm": "Replace the local file with a fresh download from the cloud?", + "unknownModel": "Unknown Model", + "verifyFile": "Check file", + "verifyFileHint": "Reads the whole audio file and checks its SHA1 hash and length. May take a while." }, "lastPlayedOnModal": { "lastPlayedOn": "Last played Tonie", @@ -2337,6 +2343,10 @@ "downloadingDetails": "Downloading \"{{model}}\" [{{ruid}}]...", "errorDuringDownload": "Error during background download", "errorDuringDownloadDetails": "Error during background download of \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Error during redownload", + "errorDuringRedownloadDetails": "Error during redownload of \"{{model}}\" [{{ruid}}]: ", + "errorDuringVerify": "Error during check", + "errorDuringVerifyDetails": "Error during check of \"{{model}}\" [{{ruid}}]: ", "errorFetchingUpdatedCard": "Error fetching updated card!", "errorFetchingUpdatedCardDetails": "Error fetching updated card for \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filter deleted", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Live flag enabled for \"{{model}}\" [{{ruid}}]", "noFilterFound": "No filter found", "noFilterFoundDetails": "No filter with name {{name}} was found", + "redownloadedFile": "Redownloaded file", + "redownloadedFileDetails": "Redownloaded \"{{model}}\" [{{ruid}}] from the cloud", + "redownloading": "Redownloading...", + "redownloadingDetails": "Redownloading \"{{model}}\" [{{ruid}}] from the cloud...", "setToEmptyValue": "empty", "setTonieToModelFailed": "Could not change tonie model!", "setTonieToModelFailedDetails": "Could not change tonie model for [{{ruid}}]: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "Could not change tonie source!", "setTonieToSourceFailedDetails": "Could not change tonie source for [{{ruid}}]: ", "setTonieToSourceSuccessful": "Set tonie source successful!", - "setTonieToSourceSuccessfulDetails": "Set tonie source to {{selectedSource}} for [{{ruid}}]" + "setTonieToSourceSuccessfulDetails": "Set tonie source to {{selectedSource}} for [{{ruid}}]", + "verifiedCorrupt": "File is corrupted", + "verifiedCorruptDetails": "The audio file of \"{{model}}\" [{{ruid}}] failed the full check. Redownload it from the cloud.", + "verifiedValid": "File is valid", + "verifiedValidDetails": "The audio file of \"{{model}}\" [{{ruid}}] passed the full check" }, "navigationTitle": "Tonies", "noData": "No Tonies/Custom tags found.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Whether this Tonie has been claimed (true/false).", + "corrupt": "Whether the audio file failed the full check (true/false).", "episode": "The episode name of the Tonie.", "exists": "Whether the Tonie exists (true/false).", "hasCloudAuth": "Whether the Tonie has cloud authorization (true/false).", diff --git a/public/translations/es.json b/public/translations/es.json index b64fd85f..30d3e1b2 100644 --- a/public/translations/es.json +++ b/public/translations/es.json @@ -1978,6 +1978,7 @@ "title": "Contenido", "toniePicture": "Imagen de Tonie" }, + "corruptHint": "El archivo de audio está dañado. Abre la información para descargarlo de nuevo desde la nube.", "currentPath": "Ruta actual: ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Seleccionar imagen" }, "infoModal": { + "corrupt": "Dañado:", "download": "Descargar TAF como *.ogg", "exists": "Existe:", "model": "Modelo:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Ocultar Tonie/Tag", "ok": "OK", - "unknownModel": "Modelo desconocido" + "redownload": "Descargar de nuevo", + "redownloadConfirm": "¿Reemplazar el archivo local por una nueva descarga desde la nube?", + "unknownModel": "Modelo desconocido", + "verifyFile": "Comprobar archivo", + "verifyFileHint": "Lee todo el archivo de audio y comprueba su hash SHA1 y su longitud. Puede tardar un poco." }, "lastPlayedOnModal": { "lastPlayedOn": "Último Tonie reproducido", @@ -2337,6 +2343,10 @@ "downloadingDetails": "Descargando \"{{model}}\" [{{ruid}}]...", "errorDuringDownload": "Error durante la descarga en segundo plano", "errorDuringDownloadDetails": "Error durante la descarga en segundo plano de \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Error al descargar de nuevo", + "errorDuringRedownloadDetails": "Error al descargar de nuevo \"{{model}}\" [{{ruid}}]: ", + "errorDuringVerify": "Error durante la comprobación", + "errorDuringVerifyDetails": "Error durante la comprobación de \"{{model}}\" [{{ruid}}]: ", "errorFetchingUpdatedCard": "Error al obtener la tarjeta actualizada", "errorFetchingUpdatedCardDetails": "Error al obtener la tarjeta actualizada para \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filtro eliminado", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Indicador en vivo habilitado para \"{{model}}\" [{{ruid}}]", "noFilterFound": "No se encontró ningún filtro", "noFilterFoundDetails": "No se encontró ningún filtro con el nombre {{name}}", + "redownloadedFile": "Archivo descargado de nuevo", + "redownloadedFileDetails": "\"{{model}}\" [{{ruid}}] se descargó de nuevo desde la nube", + "redownloading": "Descargando de nuevo...", + "redownloadingDetails": "Descargando de nuevo \"{{model}}\" [{{ruid}}] desde la nube...", "setToEmptyValue": "vacío", "setTonieToModelFailed": "No se pudo cambiar el modelo de tonie.", "setTonieToModelFailedDetails": "No se pudo cambiar el modelo de tonie para [{{ruid}}]: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "No se pudo cambiar la fuente de tonie.", "setTonieToSourceFailedDetails": "No se pudo cambiar la fuente de tonie para [{{ruid}}]: ", "setTonieToSourceSuccessful": "Fuente de tonie configurada con éxito.", - "setTonieToSourceSuccessfulDetails": "Fuente de tonie configurada como {{selectedSource}} para [{{ruid}}]" + "setTonieToSourceSuccessfulDetails": "Fuente de tonie configurada como {{selectedSource}} para [{{ruid}}]", + "verifiedCorrupt": "El archivo está dañado", + "verifiedCorruptDetails": "El archivo de audio de \"{{model}}\" [{{ruid}}] no superó la comprobación completa. Descárgalo de nuevo desde la nube.", + "verifiedValid": "El archivo es válido", + "verifiedValidDetails": "El archivo de audio de \"{{model}}\" [{{ruid}}] superó la comprobación completa" }, "navigationTitle": "Tonies", "noData": "No se encontraron Tonies/Tags personalizados.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Si este Tonie ha sido reclamado (true/false).", + "corrupt": "Si el archivo de audio no superó la comprobación completa (true/false).", "episode": "El nombre del episodio del Tonie.", "exists": "Si el Tonie existe (true/false).", "hasCloudAuth": "Si el Tonie tiene autorización en la nube (true/false).", diff --git a/public/translations/fr.json b/public/translations/fr.json index 03798f1d..1472d1f3 100644 --- a/public/translations/fr.json +++ b/public/translations/fr.json @@ -1978,6 +1978,7 @@ "title": "Contenu", "toniePicture": "Image du Tonie" }, + "corruptHint": "Le fichier audio est corrompu. Ouvrez les informations pour le télécharger à nouveau depuis le cloud.", "currentPath": "Chemin actuel : ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Sélectionner une image" }, "infoModal": { + "corrupt": "Corrompu :", "download": "Télécharger TAF en *.ogg", "exists": "Existe:", "model": "Modèle:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Masquer le Tonie/Tag", "ok": "OK", - "unknownModel": "Modèle inconnu" + "redownload": "Télécharger à nouveau", + "redownloadConfirm": "Remplacer le fichier local par un nouveau téléchargement depuis le cloud ?", + "unknownModel": "Modèle inconnu", + "verifyFile": "Vérifier le fichier", + "verifyFileHint": "Lit tout le fichier audio et vérifie son hash SHA1 et sa longueur. Peut prendre un moment." }, "lastPlayedOnModal": { "lastPlayedOn": "Dernier Tonie joué", @@ -2337,6 +2343,10 @@ "downloadingDetails": "Téléchargement de \"{{model}}\" [{{ruid}}]...", "errorDuringDownload": "Erreur lors du téléchargement en arrière-plan", "errorDuringDownloadDetails": "Erreur lors du téléchargement en arrière-plan de \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Erreur lors du nouveau téléchargement", + "errorDuringRedownloadDetails": "Erreur lors du nouveau téléchargement de \"{{model}}\" [{{ruid}}] : ", + "errorDuringVerify": "Erreur lors de la vérification", + "errorDuringVerifyDetails": "Erreur lors de la vérification de \"{{model}}\" [{{ruid}}] : ", "errorFetchingUpdatedCard": "Erreur lors de la récupération de la carte mise à jour!", "errorFetchingUpdatedCardDetails": "Erreur lors de la récupération de la carte mise à jour pour \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filtre supprimé", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Drapeau en direct activé pour \"{{model}}\" [{{ruid}}]", "noFilterFound": "Aucun filtre trouvé", "noFilterFoundDetails": "Aucun filtre avec le nom {{name}} n'a été trouvé", + "redownloadedFile": "Fichier téléchargé à nouveau", + "redownloadedFileDetails": "\"{{model}}\" [{{ruid}}] a été téléchargé à nouveau depuis le cloud", + "redownloading": "Nouveau téléchargement...", + "redownloadingDetails": "Nouveau téléchargement de \"{{model}}\" [{{ruid}}] depuis le cloud...", "setToEmptyValue": "vide", "setTonieToModelFailed": "Impossible de changer le modèle de Tonie!", "setTonieToModelFailedDetails": "Impossible de changer le modèle de Tonie pour [{{ruid}}]: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "Impossible de changer la source du Tonie!", "setTonieToSourceFailedDetails": "Impossible de changer la source du Tonie pour [{{ruid}}]: ", "setTonieToSourceSuccessful": "Source du Tonie définie avec succès!", - "setTonieToSourceSuccessfulDetails": "Source du Tonie définie sur {{selectedSource}} pour [{{ruid}}]" + "setTonieToSourceSuccessfulDetails": "Source du Tonie définie sur {{selectedSource}} pour [{{ruid}}]", + "verifiedCorrupt": "Le fichier est corrompu", + "verifiedCorruptDetails": "Le fichier audio de \"{{model}}\" [{{ruid}}] a échoué à la vérification complète. Téléchargez-le à nouveau depuis le cloud.", + "verifiedValid": "Le fichier est valide", + "verifiedValidDetails": "Le fichier audio de \"{{model}}\" [{{ruid}}] a réussi la vérification complète" }, "navigationTitle": "Tonies", "noData": "Aucun Tonies/Tags personnalisés trouvé.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Si ce Tonie a été revendiqué (true/false).", + "corrupt": "Indique si le fichier audio a échoué à la vérification complète (true/false).", "episode": "Le nom de l'épisode du Tonie.", "exists": "Si le Tonie existe (true/false).", "hasCloudAuth": "Si le Tonie a l'autorisation cloud (true/false).", diff --git a/public/translations/it.json b/public/translations/it.json index 37869dfb..0f1d56b7 100644 --- a/public/translations/it.json +++ b/public/translations/it.json @@ -1978,6 +1978,7 @@ "title": "Contenuti", "toniePicture": "Immagine Tonie" }, + "corruptHint": "Il file audio è danneggiato. Apri le informazioni per scaricarlo di nuovo dal cloud.", "currentPath": "Percorso corrente: ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Seleziona immagine" }, "infoModal": { + "corrupt": "Danneggiato:", "download": "Scarica TAF come *.ogg", "exists": "Esiste:", "model": "Modello:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Nascondi Tonie/Tag", "ok": "OK", - "unknownModel": "Modello sconosciuto" + "redownload": "Scarica di nuovo", + "redownloadConfirm": "Sostituire il file locale con un nuovo download dal cloud?", + "unknownModel": "Modello sconosciuto", + "verifyFile": "Controlla file", + "verifyFileHint": "Legge l'intero file audio e ne controlla hash SHA1 e lunghezza. Può richiedere un po' di tempo." }, "lastPlayedOnModal": { "lastPlayedOn": "Ultimo Tonie riprodotto", @@ -2337,6 +2343,10 @@ "downloadingDetails": "Download di \"{{model}}\" [{{ruid}}]...", "errorDuringDownload": "Errore durante il download in background", "errorDuringDownloadDetails": "Errore durante il download in background di \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Errore durante il nuovo download", + "errorDuringRedownloadDetails": "Errore durante il nuovo download di \"{{model}}\" [{{ruid}}]: ", + "errorDuringVerify": "Errore durante il controllo", + "errorDuringVerifyDetails": "Errore durante il controllo di \"{{model}}\" [{{ruid}}]: ", "errorFetchingUpdatedCard": "Errore durante il recupero della scheda aggiornata!", "errorFetchingUpdatedCardDetails": "Errore durante il recupero della scheda aggiornata per \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filtro eliminato", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Flag live abilitato per \"{{model}}\" [{{ruid}}]", "noFilterFound": "Nessun filtro trovato", "noFilterFoundDetails": "Non è stato trovato alcun filtro con nome {{name}}", + "redownloadedFile": "File scaricato di nuovo", + "redownloadedFileDetails": "\"{{model}}\" [{{ruid}}] è stato scaricato di nuovo dal cloud", + "redownloading": "Nuovo download...", + "redownloadingDetails": "Nuovo download di \"{{model}}\" [{{ruid}}] dal cloud...", "setToEmptyValue": "vuoto", "setTonieToModelFailed": "Impossibile modificare il modello del Tonie!", "setTonieToModelFailedDetails": "Impossibile modificare il modello del Tonie per [{{ruid}}]: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "Impossibile modificare la sorgente del Tonie!", "setTonieToSourceFailedDetails": "Impossibile modificare la sorgente del Tonie per [{{ruid}}]: ", "setTonieToSourceSuccessful": "Sorgente del Tonie impostata correttamente!", - "setTonieToSourceSuccessfulDetails": "Sorgente del Tonie impostata su {{selectedSource}} per [{{ruid}}]" + "setTonieToSourceSuccessfulDetails": "Sorgente del Tonie impostata su {{selectedSource}} per [{{ruid}}]", + "verifiedCorrupt": "Il file è danneggiato", + "verifiedCorruptDetails": "Il file audio di \"{{model}}\" [{{ruid}}] non ha superato il controllo completo. Scaricalo di nuovo dal cloud.", + "verifiedValid": "Il file è valido", + "verifiedValidDetails": "Il file audio di \"{{model}}\" [{{ruid}}] ha superato il controllo completo" }, "navigationTitle": "Tonie", "noData": "Nessun Tonie/Tag personalizzato trovato.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Indica se questo Tonie è stato assegnato (true/false).", + "corrupt": "Se il file audio non ha superato il controllo completo (true/false).", "episode": "Il nome dell'episodio del Tonie.", "exists": "Indica se il Tonie esiste (true/false).", "hasCloudAuth": "Indica se il Tonie dispone dell'autorizzazione cloud (true/false).", diff --git a/public/translations/pl.json b/public/translations/pl.json index 7104c4a7..b17bca48 100644 --- a/public/translations/pl.json +++ b/public/translations/pl.json @@ -1978,6 +1978,7 @@ "title": "Zawartość", "toniePicture": "Obraz Tonie" }, + "corruptHint": "Plik audio jest uszkodzony. Otwórz informacje, aby pobrać go ponownie z chmury.", "currentPath": "Bieżąca ścieżka: ", "customEditor": { "actions": { @@ -2295,6 +2296,7 @@ "titleSelect": "Wybierz obraz" }, "infoModal": { + "corrupt": "Uszkodzony:", "download": "Pobierz TAF jako *.ogg", "exists": "Istnieje:", "model": "Model:", @@ -2306,7 +2308,11 @@ "informationModal": { "hideTag": "Ukryj Tonie/Tag", "ok": "OK", - "unknownModel": "Nieznany model" + "redownload": "Pobierz ponownie", + "redownloadConfirm": "Zastąpić plik lokalny nowym pobraniem z chmury?", + "unknownModel": "Nieznany model", + "verifyFile": "Sprawdź plik", + "verifyFileHint": "Odczytuje cały plik audio i sprawdza jego hash SHA1 oraz długość. Może to chwilę potrwać." }, "lastPlayedOnModal": { "lastPlayedOn": "Ostatnio odtwarzany Tonie", @@ -2337,6 +2343,10 @@ "downloadingDetails": "Pobieranie \"{{model}}\" [{{ruid}}]...", "errorDuringDownload": "Błąd podczas pobierania w tle", "errorDuringDownloadDetails": "Błąd podczas pobierania w tle \"{{model}}\" [{{ruid}}]: ", + "errorDuringRedownload": "Błąd podczas ponownego pobierania", + "errorDuringRedownloadDetails": "Błąd podczas ponownego pobierania \"{{model}}\" [{{ruid}}]: ", + "errorDuringVerify": "Błąd podczas sprawdzania", + "errorDuringVerifyDetails": "Błąd podczas sprawdzania \"{{model}}\" [{{ruid}}]: ", "errorFetchingUpdatedCard": "Błąd podczas pobierania zaktualizowanej karty!", "errorFetchingUpdatedCardDetails": "Błąd podczas pobierania zaktualizowanej karty dla \"{{model}}\" [{{ruid}}]: ", "filterDeleted": "Filtr usunięty", @@ -2355,6 +2365,10 @@ "liveEnabledDetails": "Flaga live włączona dla \"{{model}}\" [{{ruid}}]", "noFilterFound": "Nie znaleziono filtru", "noFilterFoundDetails": "Nie znaleziono filtru o nazwie {{name}}", + "redownloadedFile": "Plik pobrany ponownie", + "redownloadedFileDetails": "\"{{model}}\" [{{ruid}}] został ponownie pobrany z chmury", + "redownloading": "Ponowne pobieranie...", + "redownloadingDetails": "Ponowne pobieranie \"{{model}}\" [{{ruid}}] z chmury...", "setToEmptyValue": "puste", "setTonieToModelFailed": "Nie udało się zmienić modelu tonie!", "setTonieToModelFailedDetails": "Nie udało się zmienić modelu tonie dla [{{ruid}}]: ", @@ -2363,7 +2377,11 @@ "setTonieToSourceFailed": "Nie udało się zmienić źródła tonie!", "setTonieToSourceFailedDetails": "Nie udało się zmienić źródła tonie dla [{{ruid}}]: ", "setTonieToSourceSuccessful": "Ustawienie źródła tonie zakończone pomyślnie!", - "setTonieToSourceSuccessfulDetails": "Ustawiono źródło tonie na {{selectedSource}} dla [{{ruid}}]" + "setTonieToSourceSuccessfulDetails": "Ustawiono źródło tonie na {{selectedSource}} dla [{{ruid}}]", + "verifiedCorrupt": "Plik jest uszkodzony", + "verifiedCorruptDetails": "Plik audio \"{{model}}\" [{{ruid}}] nie przeszedł pełnego sprawdzenia. Pobierz go ponownie z chmury.", + "verifiedValid": "Plik jest poprawny", + "verifiedValidDetails": "Plik audio \"{{model}}\" [{{ruid}}] przeszedł pełne sprawdzenie" }, "navigationTitle": "Tonies", "noData": "Nie znaleziono Tonies/niestandardowych tagów.", @@ -2572,6 +2590,7 @@ "customFilterHelp": { "fields": { "claimed": "Czy ten Tonie został przypisany (true/false).", + "corrupt": "Czy plik audio nie przeszedł pełnego sprawdzenia (true/false).", "episode": "Nazwa odcinka Tonie.", "exists": "Czy Tonie istnieje (true/false).", "hasCloudAuth": "Czy Tonie ma autoryzację w chmurze (true/false).", diff --git a/src/api/apis/TeddyCloudApi.ts b/src/api/apis/TeddyCloudApi.ts index d008c28a..b4451ff8 100644 --- a/src/api/apis/TeddyCloudApi.ts +++ b/src/api/apis/TeddyCloudApi.ts @@ -94,9 +94,10 @@ export class TeddyCloudApi extends runtime.BaseAPI { ruid: string, overlay?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction, + validate?: boolean, ): Promise> { const response = await this.apiGetTeddyCloudApiRaw( - `/api/getTagInfo?ruid=${ruid}`, + `/api/getTagInfo?ruid=${ruid}${validate ? "&validate=true" : ""}`, overlay, initOverrides, ); @@ -104,14 +105,15 @@ export class TeddyCloudApi extends runtime.BaseAPI { } /** - * get tag info + * get tag info, validate=true checks the SHA1 hash and length of the whole TAF */ async apiGetTagInfo( ruid: string, overlay?: string, initOverrides?: RequestInit | runtime.InitOverrideFunction, + validate?: boolean, ): Promise { - const response = await this.apiGetTagInfoRaw(ruid, overlay, initOverrides); + const response = await this.apiGetTagInfoRaw(ruid, overlay, initOverrides, validate); const tag = (await response.value()).tagInfo; return tag; diff --git a/src/components/tonies/common/modals/TonieInformationModal.tsx b/src/components/tonies/common/modals/TonieInformationModal.tsx index 19e12427..cfd381ea 100644 --- a/src/components/tonies/common/modals/TonieInformationModal.tsx +++ b/src/components/tonies/common/modals/TonieInformationModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Modal, Typography, Button, Tooltip, Flex } from "antd"; +import { Modal, Typography, Button, Tooltip, Flex, Popconfirm, Space } from "antd"; import { Record } from "../../../../types/fileBrowserTypes"; import { TonieCardProps } from "../../../../types/tonieTypes"; @@ -35,6 +35,10 @@ interface InformationModalProps { lastRUIDs?: Array<[string, string, string]>; overlay?: string; onHide?: (ruid: string) => void; + onVerify?: () => void; + isVerifying?: boolean; + onRedownload?: () => void; + isRedownloading?: boolean; } const TonieInformationModal: React.FC = ({ @@ -46,6 +50,10 @@ const TonieInformationModal: React.FC = ({ lastRUIDs, overlay, onHide, + onVerify, + isVerifying, + onRedownload, + isRedownloading, }) => { const { t } = useTranslation(); const { playAudio } = useAudioContext(); @@ -243,19 +251,40 @@ const TonieInformationModal: React.FC = ({ const informationModalFooter = (
-
- {"ruid" in tonieCardOrTAFRecord && tonieCardOrTAFRecord.ruid ? ( - !readOnly ? ( - + ) : null} + {onVerify && + !readOnly && + "exists" in tonieCardOrTAFRecord && + tonieCardOrTAFRecord.exists ? ( + + - ) : ( - "" - ) - ) : ( - "" - )} -
+ + ) : null} + {onRedownload && !readOnly ? ( + + + + ) : null} + @@ -335,13 +364,22 @@ const TonieInformationModal: React.FC = ({ {"valid" in tonieCardOrTAFRecord ? (

{t("tonies.infoModal.valid")}{" "} - {tonieCardOrTAFRecord.valid + {tonieCardOrTAFRecord.valid && + !("corrupt" in tonieCardOrTAFRecord && tonieCardOrTAFRecord.corrupt) ? t("tonies.infoModal.yes") : t("tonies.infoModal.no")}

) : ( "" )} + {"corrupt" in tonieCardOrTAFRecord && tonieCardOrTAFRecord.corrupt ? ( +

+ {t("tonies.infoModal.corrupt")}{" "} + {t("tonies.infoModal.yes")} +

+ ) : ( + "" + )} {"exists" in tonieCardOrTAFRecord ? (

{t("tonies.infoModal.exists")}{" "} diff --git a/src/components/tonies/toniecard/TonieCard.tsx b/src/components/tonies/toniecard/TonieCard.tsx index c305fd21..e1667379 100644 --- a/src/components/tonies/toniecard/TonieCard.tsx +++ b/src/components/tonies/toniecard/TonieCard.tsx @@ -10,6 +10,7 @@ import { PlayCircleOutlined, RetweetOutlined, StopOutlined, + WarningOutlined, } from "@ant-design/icons"; import { TonieCardProps, TonieInfo } from "../../../types/tonieTypes"; @@ -88,6 +89,13 @@ export const TonieCard: React.FC<{ const [keySelectFileFileBrowser, setKeySelectFileFileBrowser] = useState(0); const [isInformationModalOpen, setInformationModalOpen] = useState(false); + // result of a file check, passed to the list when the information modal closes + const [verified, setVerified] = useState<{ + checked: TonieCardProps; + card: TonieCardProps; + } | null>(null); + // a check of an older version of this card is outdated + const verifiedTonieCard = verified?.checked === tonieCard ? verified.card : null; const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isSelectFileModalOpen, setSelectFileModalOpen] = useState(false); const [selectedModelDisplayText, setSelectedModelDisplayText] = useState(""); @@ -243,7 +251,10 @@ export const TonieCard: React.FC<{ handleLiveClick, handleNoCloudClick, handleBackgroundDownload, + handleRedownload, + handleVerify, isBackgroundDownloading, + isVerifying, } = useTonieCardActions({ tonieCard, overlay, @@ -253,6 +264,7 @@ export const TonieCard: React.FC<{ addLoadingNotification, closeLoadingNotification, fetchUpdatedTonieCard, + onVerified: (card, checked) => setVerified({ checked, card }), }); // ------------------------ @@ -588,6 +600,23 @@ export const TonieCard: React.FC<{ : t("tonies.unsetTonie")}

+ {(verifiedTonieCard ?? tonieCard).corrupt ? ( + + setInformationModalOpen(true)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setInformationModalOpen(true); + } + }} + /> + + ) : null} {languageCode && languageCode !== defaultLanguageCode ? ( setInformationModalOpen(false)} - tonieCardOrTAFRecord={tonieCard} + onClose={() => { + setInformationModalOpen(false); + if (verifiedTonieCard) { + setVerified(null); + onUpdate(verifiedTonieCard); + } + }} + tonieCardOrTAFRecord={verifiedTonieCard ?? tonieCard} showSourceInfo={showSourceInfo} readOnly={readOnly} lastRUIDs={lastRUIDs} onHide={onHide} overlay={overlay} + onVerify={handleVerify} + isVerifying={isVerifying} + onRedownload={ + (verifiedTonieCard ?? tonieCard).redownloadUrl && toniesCloudAvailable + ? () => handleRedownload((verifiedTonieCard ?? tonieCard).redownloadUrl) + : undefined + } + isRedownloading={isBackgroundDownloading} /> )} diff --git a/src/components/tonies/toniecard/hooks/useTonieCardActions.ts b/src/components/tonies/toniecard/hooks/useTonieCardActions.ts index dc507216..29b7611e 100644 --- a/src/components/tonies/toniecard/hooks/useTonieCardActions.ts +++ b/src/components/tonies/toniecard/hooks/useTonieCardActions.ts @@ -20,6 +20,8 @@ type UseTonieCardActionsParams = { addLoadingNotification: (key: string, title: string, description?: string) => void; closeLoadingNotification: (key: string) => void; fetchUpdatedTonieCard: () => Promise; + // the list remounts all cards on an update, which would close an open modal + onVerified: (verifiedTonieCard: TonieCardProps, checkedTonieCard: TonieCardProps) => void; }; export const useTonieCardActions = ({ @@ -31,8 +33,10 @@ export const useTonieCardActions = ({ addLoadingNotification, closeLoadingNotification, fetchUpdatedTonieCard, + onVerified, }: UseTonieCardActionsParams) => { const [isBackgroundDownloading, setIsBackgroundDownloading] = useState(false); + const [isVerifying, setIsVerifying] = useState(false); const notifySuccess = (message: string, description: string) => { addNotification(NotificationTypeEnum.Success, message, description, t("tonies.title")); @@ -123,8 +127,13 @@ export const useTonieCardActions = ({ } }; - const handleBackgroundDownload = async () => { - const path = tonieCard.downloadTriggerUrl; + const messageArgs = () => ({ model: modelTitle, ruid: tonieCard.ruid }); + + // the server streams the TAF from the cloud and caches it; the response body itself is not needed + const runCloudDownload = async ( + path: string | undefined, + messages: { loading: string; success: string; error: string }, + ) => { if (!path || isBackgroundDownloading) return; const key = "loading" + tonieCard.ruid; @@ -133,34 +142,24 @@ export const useTonieCardActions = ({ try { addLoadingNotification( key, - t("tonies.messages.downloading"), - t("tonies.messages.downloadingDetails", { - model: modelTitle, - ruid: tonieCard.ruid, - }).replace(' "" ', " "), + t(`tonies.messages.${messages.loading}`), + t(`tonies.messages.${messages.loading}Details`, messageArgs()).replace(' "" ', " "), ); const response = await api.apiGetTeddyCloudApiRaw(path); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const blob = await response.blob(); + await response.blob(); closeLoadingNotification(key); notifySuccess( - t("tonies.messages.downloadedFile"), - t("tonies.messages.downloadedFileDetails", { - model: modelTitle, - ruid: tonieCard.ruid, - }).replace(' "" ', " "), + t(`tonies.messages.${messages.success}`), + t(`tonies.messages.${messages.success}Details`, messageArgs()).replace(' "" ', " "), ); await fetchUpdatedTonieCard(); } catch (error) { closeLoadingNotification(key); notifyError( - t("tonies.messages.errorDuringDownload"), - t("tonies.messages.errorDuringDownloadDetails", { - model: modelTitle, - ruid: tonieCard.ruid, - }).replace(' "" ', ""), + t(`tonies.messages.${messages.error}`), + t(`tonies.messages.${messages.error}Details`, messageArgs()).replace(' "" ', ""), error, ); } finally { @@ -168,10 +167,62 @@ export const useTonieCardActions = ({ } }; + const handleBackgroundDownload = () => + runCloudDownload(tonieCard.downloadTriggerUrl, { + loading: "downloading", + success: "downloadedFile", + error: "errorDuringDownload", + }); + + // the url can come from a just checked card that is not in the list yet + const handleRedownload = (redownloadUrl = tonieCard.redownloadUrl) => + runCloudDownload(redownloadUrl, { + loading: "redownloading", + success: "redownloadedFile", + error: "errorDuringRedownload", + }); + + const handleVerify = async () => { + if (isVerifying) return; + setIsVerifying(true); + const checkedTonieCard = tonieCard; + try { + const verifiedTonieCard = await api.apiGetTagInfo( + tonieCard.ruid, + overlay, + undefined, + true, + ); + onVerified(verifiedTonieCard, checkedTonieCard); + if (verifiedTonieCard.corrupt) { + notifyError( + t("tonies.messages.verifiedCorrupt"), + t("tonies.messages.verifiedCorruptDetails", messageArgs()).replace(' "" ', " "), + ); + } else { + notifySuccess( + t("tonies.messages.verifiedValid"), + t("tonies.messages.verifiedValidDetails", messageArgs()).replace(' "" ', " "), + ); + } + } catch (error) { + notifyError( + t("tonies.messages.errorDuringVerify"), + t("tonies.messages.errorDuringVerifyDetails", messageArgs()).replace(' "" ', ""), + error, + ); + } finally { + setIsVerifying(false); + } + }; + return { handleLiveClick, handleNoCloudClick, handleBackgroundDownload, + handleRedownload, + handleVerify, isBackgroundDownloading, + isVerifying, }; }; diff --git a/src/components/tonies/tonieslist/hooks/useToniesFilter.test.ts b/src/components/tonies/tonieslist/hooks/useToniesFilter.test.ts index 09aa1b57..ab0a2702 100644 --- a/src/components/tonies/tonieslist/hooks/useToniesFilter.test.ts +++ b/src/components/tonies/tonieslist/hooks/useToniesFilter.test.ts @@ -145,6 +145,19 @@ describe("useToniesFilter custom filter", () => { expect(result.map((t) => t.ruid)).toEqual(["match"]); }); + it("filters corrupted tonies, a missing corrupt flag counts as not corrupted", () => { + const corrupted = makeTonie({ ruid: "corrupt", corrupt: true }); + const intact = makeTonie({ ruid: "intact", corrupt: false }); + const unknown = makeTonie({ ruid: "unknown" }); + + expect( + applyCustomFilter("corrupt", [corrupted, intact, unknown]).map((t) => t.ruid), + ).toEqual(["corrupt"]); + expect( + applyCustomFilter("!corrupt", [corrupted, intact, unknown]).map((t) => t.ruid), + ).toEqual(["intact", "unknown"]); + }); + it("supports case-insensitive startswith, endswith and in operators", () => { const a = makeTonie({ ruid: "a" }); const b = makeTonie({ diff --git a/src/components/tonies/tonieslist/hooks/useToniesFilter.ts b/src/components/tonies/tonieslist/hooks/useToniesFilter.ts index f93c3315..3bd3d804 100644 --- a/src/components/tonies/tonieslist/hooks/useToniesFilter.ts +++ b/src/components/tonies/tonieslist/hooks/useToniesFilter.ts @@ -34,6 +34,7 @@ const FIELD_ACCESSORS: FIELD_ACCESSOR_MAP = { picture: (t) => t.sourceInfo?.picture || t.tonieInfo.picture || "", exists: (t) => t.exists, valid: (t) => t.valid, + corrupt: (t) => t.corrupt === true, live: (t) => t.live, nocloud: (t) => t.nocloud, uid: (t) => t.uid, @@ -476,7 +477,11 @@ export function useToniesFilter(params: UseToniesFilterParams) { } const suggestionsAfterField = (field: string): string[] => { - if (["exists", "valid", "live", "nocloud", "claimed", "hasCloudAuth"].includes(field)) { + if ( + ["exists", "valid", "corrupt", "live", "nocloud", "claimed", "hasCloudAuth"].includes( + field, + ) + ) { return [...LOGICALS]; } @@ -570,7 +575,15 @@ export function useToniesFilter(params: UseToniesFilterParams) { if (isNegated) { return unique.map((c) => - ["exists", "valid", "live", "nocloud", "claimed", "hasCloudAuth"].includes(c) + [ + "exists", + "valid", + "corrupt", + "live", + "nocloud", + "claimed", + "hasCloudAuth", + ].includes(c) ? `!${c}` : c, ); diff --git a/src/components/tonies/tonieslist/modals/ToniesCustomFilterHelpModal.tsx b/src/components/tonies/tonieslist/modals/ToniesCustomFilterHelpModal.tsx index 67547e6b..c12d864b 100644 --- a/src/components/tonies/tonieslist/modals/ToniesCustomFilterHelpModal.tsx +++ b/src/components/tonies/tonieslist/modals/ToniesCustomFilterHelpModal.tsx @@ -45,6 +45,10 @@ const CustomFilterHelpModal: React.FC = ({ open, onC description: t("tonies.tonies.filterBar.customFilterHelp.fields.claimed"), }, { field: "valid", description: t("tonies.tonies.filterBar.customFilterHelp.fields.valid") }, + { + field: "corrupt", + description: t("tonies.tonies.filterBar.customFilterHelp.fields.corrupt"), + }, { field: "live", description: t("tonies.tonies.filterBar.customFilterHelp.fields.live") }, { field: "nocloud", diff --git a/src/types/tonieTypes.ts b/src/types/tonieTypes.ts index c8cb6fff..f654e5ba 100644 --- a/src/types/tonieTypes.ts +++ b/src/types/tonieTypes.ts @@ -21,6 +21,8 @@ export type TonieCardProps = { source: string; audioUrl: string; downloadTriggerUrl: string; + redownloadUrl?: string; + corrupt?: boolean; tonieInfo: TonieInfo; sourceInfo: TonieInfo; trackSeconds: number[]; From d34a80cadd88c10763b6d5a529acaa8e961af446 Mon Sep 17 00:00:00 2001 From: weatherhog Date: Mon, 26 Oct 2026 21:50:01 +0100 Subject: [PATCH 2/2] chore: move changelog entries to the new 0.8.0 section --- CHANGELOG.md | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86a125e0..7c8aa365 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## 0.8.0 + +- Fixed a corrupted TAF in the library being kept when the same content was cached from the cloud again [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) +- Added `corrupt` and `redownloadUrl` to the tag index, a forced cloud download (`force=true`) and a full TAF check on demand (`getTagInfo?validate=true`) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) +- gui: Show corrupted TAF files on the Tonie card, check a TAF on demand and redownload it from the cloud in the Tonie information [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) + +### Commits + +- [https://github.com/toniebox-reverse-engineering/teddycloud/compare/tc_v0.7.1...tc_v0.8.0](https://github.com/toniebox-reverse-engineering/teddycloud/compare/tc_v0.7.1...tc_v0.8.0) +- [https://github.com/toniebox-reverse-engineering/teddycloud_web/compare/tcw_v0.7.1...tcw_v0.8.0](https://github.com/toniebox-reverse-engineering/teddycloud_web/compare/tcw_v0.7.1...tcw_v0.8.0) + +### Discussion + +- [https://forum.revvox.de/t/release-notes-0-8-0/XXXXX](https://forum.revvox.de/t/release-notes-0-8-0/XXXXX) + +### GitHub Release + +- [https://github.com/toniebox-reverse-engineering/teddycloud/releases/tag/tc_v0.8.0](https://github.com/toniebox-reverse-engineering/teddycloud/releases/tag/tc_v0.8.0) + ## 0.7.1 - Added "listened" status tracking for library files, incl. auto-mark on sync (opt-out) [https://github.com/toniebox-reverse-engineering/teddycloud/pull/467](https://github.com/toniebox-reverse-engineering/teddycloud/pull/467) @@ -16,8 +35,6 @@ - Fixed Docker startup when the configured PUID/PGID already belongs to an existing account in the base image - Greatly expanded automated backend testing and CI, including isolated test environments, C and pytest test suites, simulated Tonieboxes with hardware-specific profiles, a fake Boxine cloud and additional security regression tests [https://github.com/toniebox-reverse-engineering/teddycloud/pull/461](https://github.com/toniebox-reverse-engineering/teddycloud/pull/461) - Added and expanded architecture, settings, streaming and development documentation -- Fixed a corrupted TAF in the library being kept when the same content was cached from the cloud again [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) -- Added `corrupt` and `redownloadUrl` to the tag index, a forced cloud download (`force=true`) and a full TAF check on demand (`getTagInfo?validate=true`) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) - gui: Download TAF tracks as separate .ogg files (ZIP when more than one track is selected) [https://github.com/toniebox-reverse-engineering/teddycloud/issues/169](https://github.com/toniebox-reverse-engineering/teddycloud/issues/169) - gui: Box setup can create a port patch for CC3200 boxes (altPort.custom.json) and patch the port of ESP32 boxes, e.g. for a proxy that routes boxes by port - gui: Certificate check per box (issued by TeddyCloud / pinned / other certificate) on the box card and in the certificates dialog [https://github.com/toniebox-reverse-engineering/teddycloud/issues/85](https://github.com/toniebox-reverse-engineering/teddycloud/issues/85) @@ -58,7 +75,6 @@ - gui: Fixed TAF track downloads in the TonieInformationModal to use the correct linked/source TAF for Content and Tonies views, including uncached Tonies, and keep track selections across re-renders, screen size changes and audio player updates - gui: TeddyStudio: Reworked label printing to generate a browser-independent PDF instead of relying on CSS print layouts, including PDF preview and download functionality [https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/286](https://github.com/toniebox-reverse-engineering/teddycloud_web/issues/286) - gui: Adapted outdated hint to new release candidate tags -- gui: Show corrupted TAF files on the Tonie card, check a TAF on demand and redownload it from the cloud in the Tonie information [https://github.com/toniebox-reverse-engineering/teddycloud/issues/338](https://github.com/toniebox-reverse-engineering/teddycloud/issues/338) ### Commits