172 Commits

Author SHA1 Message Date
f5fda050ed Supprimer l'importation du gestionnaire de stockage dans le gestionnaire de vidéos 2025-04-03 11:40:22 +02:00
401deb3e69 Ajout de la gestion de l'environnement de développement avec un fichier devlock, mise à jour des scripts de démarrage et création d'un serveur local pour le développement. 2025-04-03 11:38:21 +02:00
03ec179590 Ajout de la gestion des vidéos inachevées et mise à jour des fonctions de création et de mise à jour des vidéos dans le gestionnaire de base de données. 2025-04-03 11:27:11 +02:00
6077dfd716 Refactor la gestion des mesures en remplaçant le gestionnaire de mesures par le gestionnaire de stockage. Ajouter des fonctions pour gérer les images et les chemins des mesures. Améliorer la gestion des erreurs et nettoyer le code. 2025-04-03 11:03:10 +02:00
c3b2059428 Refactor le gestionnaire de stockage pour encapsuler les fonctions de création et de suppression de répertoires de projet dans un objet. Mettre à jour les routes pour utiliser la nouvelle structure. 2025-04-03 10:31:29 +02:00
0d0c101e20 Renommer le gestionnaire de stockage et mettre à jour les références dans les fichiers concernés. Supprimer les fichiers obsolètes et ajouter un nouveau fichier de gestion de stockage. 2025-04-03 10:29:17 +02:00
915146c140 Ajouter la gestion de la base de données avec des fonctions pour créer, vérifier et supprimer des tables. Mettre à jour la connexion à la base de données pour un environnement de développement. Améliorer l'initialisation de la caméra et corriger l'appel des fonctions asynchrones. 2025-04-03 09:54:29 +02:00
242bbcd597 Renommer la fonction de connexion à la base de données et améliorer la gestion des erreurs. Nettoyer le code en supprimant les commentaires inutiles et réinitialiser le compteur après la journalisation des modifications. 2025-04-03 09:07:45 +02:00
a33e517a8a Nettoyer le code en supprimant les importations inutilisées et en décommentant des fonctions pour améliorer la lisibilité. 2025-04-03 09:02:34 +02:00
ed853ab0f7 Rendre plusieurs fonctions asynchrones pour améliorer la gestion des erreurs et assurer un traitement correct des opérations liées à la caméra et aux projets. 2025-04-03 08:41:25 +02:00
12898d67c0 Activer le changement de statut du projet dans la fonction stopProcedure pour marquer le projet comme terminé. 2025-04-02 11:07:20 +02:00
4642c8cca6 Modifier la fonction resetProjectStatus pour changer le statut des projets de 1 à 2 et décommenter son appel dans stopProcedure. 2025-04-02 11:07:00 +02:00
daca488532 Rendre la fonction changeProjectStatus asynchrone pour améliorer la gestion des erreurs et l'intégrer dans la fonction stopProcedure. 2025-04-02 11:04:59 +02:00
3d00f6afbf Rendre la fonction stopProcedure asynchrone pour améliorer la gestion des erreurs lors de l'arrêt de la procédure. 2025-04-02 11:02:21 +02:00
15692a3fc8 Commenter la réinitialisation du statut du projet dans la fonction stopProcedure pour éviter des réinitialisations non souhaitées. 2025-04-02 11:02:04 +02:00
dd03db42a9 Rendre la fonction startProcedure asynchrone dans la route de démarrage de la procédure pour améliorer la gestion des erreurs. 2025-04-02 10:54:03 +02:00
a0b1eaf109 Modification de la fonction startProcedure pour la rendre asynchrone et améliorer la gestion de l'occupation de la caméra. 2025-04-02 10:52:17 +02:00
b65230d5e7 Ajout de la documentation Swagger pour les procédures de capture et restauration, et réintégration de la fonction de démarrage de la procédure avec gestion des erreurs. 2025-04-02 10:48:56 +02:00
55b4c04187 Modification de la fonction getCamera pour utiliser une exécution asynchrone et amélioration de la gestion des erreurs 2025-04-02 10:46:29 +02:00
61cdb25398 Modification de la fonction getCamera pour utiliser une exécution synchrone avec querySync 2025-04-02 10:42:00 +02:00
c4d62c473e Mise à jour de la requête de suppression des vidéos inachevées pour inclure les statuts 0 et 2 2025-04-02 10:40:16 +02:00
7dafdcecde Mise à jour du statut de la vidéo lors de la création d'un projet vidéo : ajout de nouveaux états pour refléter les différentes phases de création. 2025-04-02 10:39:32 +02:00
d1b75329ea Ajout de la fonction de nettoyage des fichiers vidéo non associés et appel de la fonction de suppression des vidéos inachevées 2025-04-02 10:34:27 +02:00
90e036b150 Supprimer les vidéos inachevées au démarrage du backend 2025-04-02 10:33:42 +02:00
aa9a21c638 Ajout de la vérification de l'occupation de la caméra et réinitialisation des statuts des projets lors du redémarrage du backend. Mise à jour des paramètres de la caméra et modification du statut des vidéos. 2025-04-02 10:28:59 +02:00
90ce92b90b Désactiver les logs d'information dans la fonction de vérification des entrées invalides 2025-04-02 09:56:35 +02:00
647dd72b5b Nettoyage des routes : suppression des anciennes définitions Swagger et des variables inutilisées dans cameraRoutes.js 2025-04-02 09:50:08 +02:00
73922d8afc Ajouter une route pour récupérer l'état actuel de la caméra 2025-04-02 09:28:11 +02:00
293245d457 Ajouter l'option de combinaison des logs dans la configuration du backend 2025-04-02 09:27:28 +02:00
368abfbeca Initialisation de la caméra avec des valeurs par défaut et ajout de la récupération de l'état de la caméra 2025-04-02 09:23:34 +02:00
38864a68d8 Supprimer l'ancienne route pour obtenir les procédures dans uploadRoutes.js 2025-03-31 11:47:20 +02:00
71cb9898bb Résolution de la gestion vidéo #3 2025-03-31 11:35:37 +02:00
9101497a7f Ajouter la prise en charge de la résolution personnalisée lors de la création de vidéos 2025-03-31 11:22:19 +02:00
6c48612554 Ajouter des options de redimensionnement et de bitrate pour la création de vidéos 2025-03-31 11:03:38 +02:00
bb51208d06 Merge branch 'main' of gitea.kerboul.me:timelapse/timelapse-backend 2025-03-31 10:48:08 +02:00
c2dcf3fa13 Simplifier la commande de démarrage dans le Dockerfile pour utiliser uniquement backend.config.js et modifier la réponse de création de vidéo pour inclure uniquement l'ID. 2025-03-31 10:47:56 +02:00
eb47639397 Afficher le corps de la requête lors du téléchargement de mesures 2025-03-31 08:26:06 +00:00
582fd87f32 Ajouter des journaux pour la suppression de vidéos et gérer le cas où aucun fichier vidéo n'est trouvé 2025-03-31 08:19:50 +00:00
2c9f81975f Modifier la commande de démarrage dans le Dockerfile pour utiliser backend.config.js avec PM2 2025-03-31 10:10:44 +02:00
5c7116af7a Modifier la commande de démarrage dans le Dockerfile pour utiliser un fichier de configuration PM2 et ajouter un fichier de configuration backend. 2025-03-31 10:08:39 +02:00
c91d11567c Corriger la syntaxe de la commande de démarrage dans le Dockerfile pour le délai de redémarrage. 2025-03-31 10:00:40 +02:00
5024859b6c Modifier la commande de démarrage dans le Dockerfile pour exécuter server.js avec PM2. 2025-03-31 09:58:14 +02:00
fda18fb1c6 Modifier la commande de démarrage de l'application pour utiliser PM2 avec une politique de redémarrage et ajuster le fichier docker-compose pour supprimer l'ancienne commande. 2025-03-31 09:55:04 +02:00
7536d98330 Installer pm2 globalement dans le Dockerfile pour la gestion des processus 2025-03-31 09:50:42 +02:00
fb1bdbd182 Ajouter une commande pour démarrer le serveur avec pm2-runtime, incluant la surveillance et un délai de redémarrage. 2025-03-31 09:48:04 +02:00
e745c78b25 Corriger une erreur de typographie dans la définition de la route pour récupérer les procédures. 2025-03-31 09:46:36 +02:00
8c35aab855 Ajouter une route pour récupérer les procédures, retournant un JSON avec les paramètres de la procédure. 2025-03-31 09:45:41 +02:00
559ef44cb3 Corriger la suppression de vidéo pour ne pas tenter de supprimer un fichier si le chemin est nul, améliorant ainsi la gestion des erreurs. 2025-03-31 09:43:52 +02:00
411ea7a904 Ajouter une vérification pour le chemin du fichier vidéo, en utilisant une vidéo de secours si le chemin est nul ou indéfini, améliorant ainsi la robustesse du service. 2025-03-31 09:41:43 +02:00
7942a025e8 Refactor la gestion des fichiers vidéo pour utiliser un chemin de vidéo par défaut en cas d'absence de fichier, améliorant ainsi la robustesse du service. 2025-03-31 09:39:30 +02:00
3849042869 Refactor la gestion des vidéos pour utiliser une vidéo de secours en cas d'absence de fichier, améliorant ainsi la robustesse du service. 2025-03-31 09:36:05 +02:00
6747062f0b Actualiser stuff.md
C'est pour lancer la pipeline
2025-03-31 06:22:44 +00:00
c93eed9d52 Refactor la route de prévisualisation d'image pour améliorer la gestion des erreurs et intégrer le redimensionnement d'image dans des fonctions séparées 2025-03-13 12:13:48 +01:00
884e312ef7 Refactor la fonction de prévisualisation d'image pour intégrer le redimensionnement directement dans la route, améliorant ainsi la lisibilité et la gestion des erreurs. 2025-03-13 12:12:38 +01:00
5ffa1ec839 Ajouter une fonction de prévisualisation d'image pour redimensionner et renvoyer une image JPEG 2025-03-13 12:10:21 +01:00
df219bfc06 Améliorer la création de vidéos en démarrant le rendu immédiatement et en ajoutant une réponse immédiate avec l'état de traitement. 2025-03-13 12:01:52 +01:00
c3e78b248f Ajouter une route pour récupérer la progression de la création de vidéos et améliorer la gestion des erreurs dans la fonction createVideoWithList 2025-03-13 11:50:31 +01:00
a069acfce7 Modifier le champ de la base de données pour mettre à jour le chemin du fichier vidéo dans la fonction createVideoWithList 2025-03-13 11:33:44 +01:00
c90ff42961 Refactor la création de vidéos pour utiliser des promesses et améliorer la gestion des erreurs, avec une réponse immédiate au démarrage du rendu. 2025-03-13 11:28:17 +01:00
2e552be9db Modifier la création de vidéos pour utiliser des promesses et gérer les erreurs lors de l'exécution de ffmpeg 2025-03-13 10:06:48 +01:00
4bda54b529 Ajouter une fonction pour vérifier si la caméra est occupée et mettre à jour la gestion des fichiers vidéo 2025-03-13 10:02:49 +01:00
d93b2c6b7c Aucune modification apportée au code 2025-03-13 09:37:43 +01:00
dab93cfdf9 Améliorer la gestion des erreurs en ajoutant un code d'état HTTP aux réponses d'erreur dans plusieurs routes et en modifiant la fonction sendError pour accepter un code d'état personnalisé. 2025-03-13 09:32:02 +01:00
62e8aee6bd Ajouter une fonction pour écrire les paramètres de capture dans un fichier camera.txt 2025-03-13 09:24:23 +01:00
6c77d267e6 Supprimer les tests et les commentaires obsolètes dans tester.js 2025-03-13 09:07:56 +01:00
9d9868e26b Supprimer la documentation Swagger obsolète dans plusieurs fichiers de routes 2025-03-13 09:06:38 +01:00
217f0b4fd3 Corriger la description de l'API pour la suppression d'un projet en précisant "par son ID" 2025-03-13 08:55:31 +01:00
d8f1d353c6 Déplacer la documentation Swagger pour la suppression de projet vers un fichier séparé et supprimer les commentaires obsolètes dans projectRoutes.js 2025-03-13 08:53:19 +01:00
30f05ffcbe Modifier l'importation de child_process pour utiliser execSync au lieu de spawn dans videoManager.js 2025-03-10 18:01:52 +01:00
ef90f77a11 Refactor la fonction createVideoWithList pour simplifier la création de vidéos et mettre à jour le statut de la vidéo à "completed" 2025-03-10 18:00:30 +01:00
e38718b1fa videoManager.js: add videoManager.js 2025-03-10 17:56:56 +01:00
f85cead1dd Commenter les logs de la base de données dans les fonctions de gestion des projets 2025-03-10 17:53:04 +01:00
3469c757ec Modifier la route de rendu vidéo pour utiliser GET, ajouter la gestion de l'accès au fichier vidéo et renvoyer le contenu vidéo en fonction de la plage demandée 2025-03-10 17:45:42 +01:00
9ec8ff73f3 Modifier la route de rendu vidéo pour utiliser POST, ajouter la gestion des erreurs et démarrer le processus de rendu vidéo 2025-03-10 17:42:32 +01:00
55697fc032 Modifier la fonction createVideoWithList pour inclure l'ID de la vidéo et mettre à jour le statut de la vidéo après le rendu 2025-03-10 17:36:39 +01:00
7baac5dcb7 Modifier la fonction createVideoWithList pour utiliser spawn au lieu de execSync pour l'exécution de ffmpeg en arrière-plan 2025-03-10 17:31:11 +01:00
848c50bf33 Ajouter la gestion des erreurs lors de l'exécution de la commande ffmpeg pour la création de vidéos 2025-03-10 17:25:59 +01:00
81c4470464 Démarrer la création de vidéo en arrière-plan pour améliorer la réactivité 2025-03-10 17:23:52 +01:00
29f198cd85 Ajouter la durée à la requête de sélection des vidéos lors du rendu 2025-03-10 17:22:07 +01:00
3d560cfb77 Render video débug 2025-03-10 17:19:02 +01:00
553a934563 Modifier la fonction createVideoWithList pour convertir la durée en entier avant de calculer le frame rate 2025-03-10 17:16:23 +01:00
9e850f0090 Modifier la fonction createVideoWithList pour accepter la durée et ajuster le frame rate en conséquence 2025-03-10 17:08:11 +01:00
e9fd9dfaa1 Modifier la route de téléchargement de vidéo pour gérer le streaming avec prise en charge des plages de fichiers 2025-03-10 16:55:59 +01:00
8f69705ae9 Modifier la route de téléchargement de vidéo pour vérifier l'existence du fichier avant de le télécharger 2025-03-10 16:54:13 +01:00
5979cded02 Modifier la route de téléchargement de vidéo pour utiliser un flux de fichiers et gérer les erreurs de streaming 2025-03-10 16:49:33 +01:00
37d82d1133 Modifier la taille de l'image redimensionnée à un septième de ses dimensions d'origine et ajuster la qualité JPEG 2025-03-10 16:41:50 +01:00
a15ebb0697 Modifier la route de création de vidéo pour utiliser async/await et retourner l'ID de la vidéo créée 2025-03-10 16:36:06 +01:00
d17c96479f Modifier la requête d'insertion pour retourner l'ID de la nouvelle vidéo créée 2025-03-10 16:34:53 +01:00
727c28d312 Modifier la taille de l'image redimensionnée à un cinquième de ses dimensions d'origine 2025-03-10 16:13:49 +01:00
d790626a1a Ajouter une route pour prévisualiser une image redimensionnée par ID de projet et ID de commande 2025-03-10 16:12:16 +01:00
9cd1b230fd Ajouter la bibliothèque sharp et configurer les volumes pour node_modules dans docker-compose 2025-03-10 16:07:49 +01:00
a6a2492842 Ajouter la bibliothèque sharp pour le traitement d'images 2025-03-10 16:01:47 +01:00
65fcf1fc68 Mettre à jour le statut du projet dans la base de données lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:25:08 +01:00
3bf001bb58 Modifier la signature de la fonction stopProcedure pour inclure l'objet req dans la route 2025-03-10 15:20:13 +01:00
961b72b24b Ajouter des logs pour afficher les anciens et nouveaux ID de projet et intervalles lors de l'arrêt de la procédure de capture 2025-03-10 15:18:57 +01:00
a39bb6e6c0 Ajouter la vérification d'un projet en cours et améliorer les messages de retour lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:15:26 +01:00
b696897cfc Commenter la vérification de l'intervalle maximum dans la procédure de capture 2025-03-10 15:13:09 +01:00
1457711d8f Ajouter la vérification de l'existence du projet et améliorer la gestion des erreurs dans la procédure de capture 2025-03-10 15:11:42 +01:00
e446724ecd Améliorer la gestion des erreurs lors du démarrage et de l'arrêt de la procédure de capture 2025-03-10 15:08:08 +01:00
557be4a58b Modifier la méthode HTTP de la route /procedure/stop/ de GET à POST 2025-03-10 15:04:22 +01:00
0c56fd79bc Déplacer la logique de capture de cameraRoutes.js vers imageRoutes.js et ajouter les routes pour démarrer et arrêter la procédure de capture 2025-03-10 15:02:50 +01:00
44d1d6a24e Déplacer la logique de capture de cameraRoutes.js vers imageRoutes.js 2025-03-10 14:55:32 +01:00
8319ae9685 Supprimer l'utilisation de cameraRoutes dans api.js 2025-03-10 14:54:21 +01:00
4807579846 Supprimer l'importation de cameraRoutes dans api.js 2025-03-10 14:53:34 +01:00
ac0bd807df Export Default Router 2025-03-10 14:52:26 +01:00
39a7b897bf Configurer Docker avec un Dockerfile et mettre à jour docker-compose.yml pour utiliser la construction d'image 2025-03-10 14:46:29 +01:00
7785bfa10f Corriger l'importation de la base de données dans cameraRoutes.js 2025-03-10 14:42:01 +01:00
23295f13d7 Réorganiser les importations dans api.js et cameraRoutes.js 2025-03-10 14:40:06 +01:00
fe884cb8e7 Corriger le chemin d'importation de la base de données dans cameraRoutes.js 2025-03-10 14:36:47 +01:00
8ffde922fa Ajouter les routes de caméra dans le fichier api.js 2025-03-10 14:34:58 +01:00
6b95665974 Ajouter l'importation de ffmpeg dans le fichier api.js 2025-03-10 14:30:01 +01:00
6ecd573751 Ajouter un log pour le chemin à partir des IDs de projet et de commande 2025-03-10 14:22:35 +01:00
f3ed511543 Ajouter des routes pour démarrer et arrêter la procédure de capture 2025-03-10 14:19:48 +01:00
348509fddb Ajouter une route pour récupérer une image par ID de mesure 2025-03-10 13:26:46 +01:00
98e74d22f2 Modifier le nom du conteneur pour l'environnement de développement de l'API Timelapse 2025-03-10 12:05:52 +01:00
51db325dad Modifier le nom du conteneur pour l'environnement de développement de l'API Timelapse 2025-03-10 12:02:38 +01:00
7c5041b5c4 Supprimer la version de Docker Compose dans le fichier de configuration 2025-03-10 11:23:46 +01:00
2ee5897426 Modifier l'adresse IP du serveur de base de données dans la configuration 2025-03-10 11:19:08 +01:00
6178e7cdbf Supprimer le fichier de configuration GitLab CI pour le déploiement de l'API Timelapse 2025-03-10 11:03:29 +01:00
a65fcf0c47 Ajouter un fichier docker-compose pour configurer l'environnement de développement de l'API Timelapse 2025-03-10 10:54:07 +01:00
a8494ad382 Supprimer la configuration locale de la connexion à la base de données et nettoyer le code de la route de téléchargement d'images 2025-02-12 14:22:16 +01:00
dcbf2a1f00 Modifier la gestion des erreurs lors de la création de vidéos et commenter le code de test associé 2025-02-12 14:04:28 +01:00
2450359710 Ajouter une route pour réinitialiser le statut d'une vidéo par ID et supprimer le fichier vidéo associé 2025-02-12 11:50:18 +01:00
7c342c3b69 Modifier la route de récupération de vidéo pour utiliser res.download au lieu de res.sendFile 2025-02-12 11:42:15 +01:00
3f5317ad18 Corriger la récupération du chemin vidéo en utilisant video_file au lieu de video_path 2025-02-12 11:40:42 +01:00
7652a1ea64 Ajouter un log pour afficher le chemin de la vidéo dans la route de récupération 2025-02-12 11:38:57 +01:00
41c877f072 Ajouter la mise à jour du statut de la vidéo après le rendu et améliorer la gestion des erreurs 2025-02-12 11:37:33 +01:00
f99b0c60ce Refactor la route de rendu vidéo pour utiliser async/await et améliorer la gestion des erreurs 2025-02-12 11:35:33 +01:00
aa571e5149 Améliorer la gestion des erreurs lors de la mise à jour du fichier vidéo dans la route de rendu 2025-02-12 11:33:16 +01:00
ef09fdb1b4 Réorganiser la logique de rendu vidéo pour gérer les erreurs et améliorer la lisibilité du code 2025-02-12 11:26:57 +01:00
e61f1e9773 Remplacer l'appel à createVideoWithList par videoManager.createVideoWithList dans la route de rendu vidéo 2025-02-12 11:24:22 +01:00
a63e79e26e Ajouter la mise à jour du fichier vidéo et gérer les erreurs lors du rendu 2025-02-12 11:23:30 +01:00
269ad2283d Ajouter la gestion des erreurs lors de la conversion de la chaîne d'identifiants en tableau dans getPathList 2025-02-12 11:18:47 +01:00
c17c939b9c Parser les identifiants dans getPathList pour assurer un traitement correct des valeurs 2025-02-12 11:16:12 +01:00
b2e14b169f Modifier le type de contenu de la réponse pour la route /cat et simplifier le gestionnaire en supprimant le paramètre req 2025-02-12 11:09:51 +01:00
27f06daaaf Ajouter les modules fs et path dans videoRoutes pour la gestion des fichiers 2025-02-12 11:07:05 +01:00
208b6d5b28 Ajouter dbTester aux routes vidéo pour les tests 2025-02-12 11:06:17 +01:00
eb63c84443 Modifier la fonction getPathList pour utiliser orderId au lieu de id lors de la récupération des chemins 2025-02-12 11:05:25 +01:00
8b0de65272 Ajouter le project_id à la requête de récupération des vidéos et mettre à jour la fonction getPathList pour l'utiliser 2025-02-12 10:59:54 +01:00
bc2159f5f9 Modifier le gestionnaire de vidéo pour utiliser measureManager lors de la récupération de la liste des chemins 2025-02-12 10:55:31 +01:00
1e59f5ead1 Modifier la requête pour récupérer le chemin du fichier vidéo dans la route GET /videos/file/:video_id 2025-02-12 10:53:48 +01:00
f833f21b01 Ajouter une route pour rendre une vidéo par ID avec gestion des erreurs 2025-02-12 10:52:49 +01:00
1e7ae35c8a Ajouter une route pour récupérer un fichier vidéo par ID avec gestion des erreurs 2025-02-12 10:51:40 +01:00
c0215643ea Modifier le type de measurement_ids en chaîne et simplifier le traitement lors de la création d'une vidéo 2025-02-12 10:44:57 +01:00
25c056c3d8 Formater les IDs de mesure lors de la création d'une vidéo dans videoManager 2025-02-12 10:40:43 +01:00
8b45c5feb8 Ajouter un traitement d'erreur pour la création de vidéos dans la route POST /videos 2025-02-12 10:29:47 +01:00
a09805c5f1 Ajouter le champ de statut lors de la création d'une vidéo dans videoManager 2025-02-12 10:26:24 +01:00
7179d94527 Ajouter un champ de statut lors de la création d'une vidéo dans la base de données 2025-02-12 10:25:17 +01:00
b752595781 Renommer la fonction createVideo en createVideoProject dans la route POST /videos pour plus de clarté 2025-02-12 10:21:37 +01:00
3b4d8a9e5a Remplacer le traitement des erreurs par des logs dans les routes de création et de suppression de vidéos 2025-02-12 10:19:28 +01:00
2766a1d788 Ajouter des fonctions pour créer et supprimer des vidéos dans videoManager et mettre à jour la route POST /videos pour utiliser ces nouvelles fonctions 2025-02-12 10:17:08 +01:00
78708e4eaa Ajouter des logs pour le parsing des IDs de mesures dans la route POST /videos 2025-02-12 10:11:27 +01:00
27ada11471 Améliorer le parsing des IDs de mesures dans la route POST /videos pour gérer les erreurs de format 2025-02-12 10:10:54 +01:00
7aae1aaf34 Supprimer le parsing des IDs de mesures dans la route POST /videos 2025-02-12 10:07:58 +01:00
f9de2227dc Ajouter un logging pour afficher les détails de la création d'une vidéo dans la route POST /videos 2025-02-12 10:07:02 +01:00
ed4a37e259 Modifier les types de project_id, measurement_ids et duration dans la route POST /videos et ajouter le parsing des IDs de mesures 2025-02-12 10:05:57 +01:00
0c91f7d3c3 Ajouter un logging pour afficher les détails de la création d'une vidéo dans la route POST /videos 2025-02-12 10:02:39 +01:00
afe3c163f1 Modifier le type de project_id et duration en chaîne de caractères dans la documentation de la route GET /videos 2025-02-12 09:59:51 +01:00
0f31b5019f Modifier le type de measurement_ids en tableau d'entiers et simplifier la validation des IDs dans la route POST /videos 2025-02-12 09:26:57 +01:00
0a6fbb22bf Modifier le logging pour afficher uniquement le premier ID de mesure lors de la création d'une vidéo 2025-02-12 09:19:27 +01:00
6fedbe10c8 Ajouter la validation et le parsing des IDs de mesures dans la route POST /videos 2025-02-12 09:18:27 +01:00
cec3a10b2b Ajouter la documentation Swagger pour la route POST /videos afin de clarifier les paramètres et les réponses 2025-02-12 09:14:18 +01:00
2a24864003 Modifier la route POST /videos pour simplifier la validation des champs et mettre à jour la documentation Swagger 2025-02-12 09:12:11 +01:00
6ee50ee7b4 Ajouter la fonction getPathList pour récupérer les chemins à partir d'une liste d'IDs 2025-02-12 09:09:11 +01:00
158a288dec Décommenter le code de création et de suppression de vidéos dans tester.js 2025-02-12 09:06:06 +01:00
cd1f91589b Améliorer la gestion des erreurs et assurer l'attente lors de la création de dossiers et de l'enregistrement d'images dans uploadMeasureImage 2025-02-12 08:01:11 +00:00
0600fb44c2 Décommenter la fonction getSmileImage dans tester.js 2025-02-12 07:58:17 +00:00
1f21c288ff Merge branch 'main' of gitea.kerboul.me:timelapse/timelapse-backend 2025-02-12 07:57:42 +00:00
152f4ee508 Commenter le code de test dans tester.js pour désactiver les fonctions de création et de suppression de dossiers 2025-02-12 07:57:41 +00:00
c050a1744f Ajout Logs routes 2025-02-12 08:56:49 +01:00
efaa49912e Passage DB en Prod 2025-02-11 22:53:05 +01:00
bd9a9b70a1 Merge pull request 'Ajout des fonctionnalités de traitement vidéo' (#2) from dev2 into main
Reviewed-on: https://gitea.kerboul.me/timelapse/timelapse-backend/pulls/2
2025-02-11 21:51:35 +00:00
30 changed files with 3782 additions and 1206 deletions

View File

@@ -1,15 +0,0 @@
stages:
- deploy
deploy_timelapse:
stage: deploy
script:
- apt-get update && apt-get install -y openssh-client
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan 172.17.0.1 >> ~/.ssh/known_hosts
- ssh kerboul@172.17.0.1 "cd /home/kerboul/scripts/timelapse && ./update_timelapse.sh"
only:
- main

24
Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
# Utiliser une image de base officielle de Node.js
FROM node:latest
# Installer ffmpeg
RUN apt-get update && apt-get install -y ffmpeg
# Définir le répertoire de travail dans le conteneur
WORKDIR /backend
# Copier le fichier package.json et package-lock.json (si disponible)
COPY package*.json ./
# Installer les dépendances Node.js
RUN npm install
RUN npm install -g pm2
# Copier le reste de l'application
COPY . .
# Exposer le port sur lequel l'application va tourner
EXPOSE 3000
# Commande pour démarrer l'application avec PM2
CMD ["pm2-runtime", "start", "backend.config.js"]

5
api.js
View File

@@ -5,8 +5,10 @@ const projectRoutes = require('./routes/projectRoutes');
const measurementRoutes = require('./routes/measurementRoutes'); const measurementRoutes = require('./routes/measurementRoutes');
const videoRoutes = require('./routes/videoRoutes'); const videoRoutes = require('./routes/videoRoutes');
const imageRoutes = require('./routes/imageRoutes'); const imageRoutes = require('./routes/imageRoutes');
const cameraRoutes = require('./routes/cameraRoutes');
const uploadRoutes = require('./routes/uploadRoutes'); const uploadRoutes = require('./routes/uploadRoutes');
const fileWatcher = require('./src/data/filewatcher.js'); const FileWatcher = require('./src/data/filewatcher');
const database_manager = require('./src/database/database_manager');
router.use(cors({ router.use(cors({
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'], origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
@@ -20,5 +22,6 @@ router.use('/', measurementRoutes);
router.use('/', videoRoutes); router.use('/', videoRoutes);
router.use('/', imageRoutes); router.use('/', imageRoutes);
router.use('/', uploadRoutes); router.use('/', uploadRoutes);
router.use('/', cameraRoutes);
module.exports = router; module.exports = router;

10
backend.config.js Normal file
View File

@@ -0,0 +1,10 @@
module.exports = {
apps: [{
name: "backend",
script: "server.js",
out_file: "/dev/stdout",
error_file: "/dev/stderr",
log_date_format: "YYYY-MM-DD HH:mm:ss",
combine_logs: true, // Combine les logs stdout et stderr
}]
};

30
db.js
View File

@@ -1,25 +1,39 @@
const { Client } = require('pg'); const { Client } = require('pg');
const devlock = require('./devlock.js');
const local = true; let dev = devlock.is_dev;
// Connexion à la base de données PostgreSQL console.log('[INFO] Environment:', dev ? 'Development Local' : 'Development Remote');
const client = new Client({
host: local ? 'mikoshi' : '172.30.0.2', let client = new Client({
port: local ? 54322 : 5432, host: '192.168.192.3',
port: 5432,
user: 'timelapse', user: 'timelapse',
password: 'timelapse', password: 'timelapse',
database: 'timelapse' database: 'timelapse'
}); });
function connectWithRetry() { if (dev) {
client = new Client({
host: 'mikoshi',
port: 54322,
user: 'timelapse',
password: 'timelapse',
database: 'timelapse_dev'
});
}
function init_database() {
console.log('[DB] Initialisation de la base de données PostgreSQL...');
client.connect(err => { client.connect(err => {
if (err) { if (err) {
console.error('Erreur de connexion à la base de données:', err); console.error('Erreur de connexion à la base de données:', err);
setTimeout(connectWithRetry, 30000); // Réessayer après 30 secondes setTimeout(init_database, 3000);
} else { } else {
console.log('[DB] Connecté à la base de données PostgreSQL.'); console.log('[DB] Connecté à la base de données PostgreSQL.');
} }
}); });
} }
connectWithRetry(); init_database();
module.exports = client; module.exports = client;

5
devlock.js Normal file
View File

@@ -0,0 +1,5 @@
let is_dev = false; // Set to true for development mode
module.exports = {
is_dev
}

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
services:
timelapse-api:
build:
context: . # Chemin vers le répertoire contenant le Dockerfile
dockerfile: Dockerfile # Nom du Dockerfile, par défaut c'est "Dockerfile"
container_name: timelapse-api
ports:
- "8053:3000"
volumes:
- /home/timelapse/backend:/backend
- /home/timelapse/storage:/storage
- node_modules:/backend/node_modules
environment:
- NODE_VERSION=22.9.0
- YARN_VERSION=1.22.22
working_dir: /backend
restart: always
networks:
- bridge
- timelapse_network
networks:
bridge:
driver: bridge
timelapse_network:
driver: bridge
volumes:
node_modules:

11
eslint.config.mjs Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "eslint/config";
import globals from "globals";
import js from "@eslint/js";
export default defineConfig([
{ files: ["**/*.{js,mjs,cjs}"] },
{ files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
{ files: ["**/*.{js,mjs,cjs}"], languageOptions: { globals: globals.browser } },
{ files: ["**/*.{js,mjs,cjs}"], plugins: { js }, extends: ["js/recommended"] },
]);

View File

@@ -1,9 +1,9 @@
const { exec } = require('child_process'); const { exec } = require('child_process');
exec('ffmpeg -version', (error, stdout, stderr) => { exec('ffmpeg -version', (error) => {
if (error) { if (error) {
console.log('FFmpeg is not installed. Installing FFmpeg...'); console.log('FFmpeg is not installed. Installing FFmpeg...');
exec('apt update && apt install -y ffmpeg', (installError, installStdout, installStderr) => { exec('apt update && apt install -y ffmpeg', (installError) => {
if (installError) { if (installError) {
console.error(`Error installing FFmpeg: ${installError}`); console.error(`Error installing FFmpeg: ${installError}`);
return; return;

1540
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "nodemon server.js" "dev": "nodemon server.js",
"local" : "nodemon server_local.js"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@@ -16,11 +17,16 @@
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"mysql2": "^3.11.3", "mysql2": "^3.11.3",
"pg": "^8.13.0", "pg": "^8.13.0",
"range-parser": "^1.2.1",
"sharp": "^0.33.5",
"swagger-jsdoc": "^6.2.8", "swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.1" "swagger-ui-express": "^5.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.23.0",
"@types/cors": "^2.8.17", "@types/cors": "^2.8.17",
"eslint": "^9.23.0",
"globals": "^16.0.0",
"nodemon": "^3.1.7" "nodemon": "^3.1.7"
} }
} }

700
routes/_swaggerRoutes.js Normal file
View File

@@ -0,0 +1,700 @@
// Routes SWAGGER - Documentation API
// Documentation: https://swagger.io/docs/specification/2-0/basic-structure/
// Les documentations sont volontairement séparées du code source pour des raisons de lisibilité et de maintenabilité.
/**
* @swagger
* /projects/{id}:
* delete:
* summary: Supprimer un projet par son ID
* description: Supprime un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Projet supprimé avec succès.
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects:
* get:
* summary: Récupérer tous les projets
* description: Récupère tous les projets disponibles.
* responses:
* 200:
* description: Une liste de projets.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}:
* get:
* summary: Récupérer un projet par ID
* description: Récupère un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Un projet.
* content:
* application/json:
* schema:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}/videos:
* get:
* summary: Récupérer les vidéos d'un projet par ID
* description: Récupère les vidéos associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects/{id}/measurements:
* get:
* summary: Récupérer les mesures d'un projet par ID
* description: Récupère les mesures associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /projects:
* post:
* summary: Ajouter un nouveau projet
* description: Ajoute un nouveau projet à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Projet ajouté avec succès.
* 400:
* description: Le nom et la description sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /smile:
* get:
* summary: Retrieve a smile image
* responses:
* 200:
* description: A smile image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /images/{projectId}/{orderId}:
* get:
* summary: Retrieve an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /images/{measurementId}:
* get:
* summary: Retrieve an image by measurement ID
* parameters:
* - in: path
* name: measurementId
* required: true
* schema:
* type: string
* description: The measurement ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
/**
* @swagger
* /preview/{projectId}/{orderId}:
* get:
* summary: Retrieve a preview of an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: A resized preview of the image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
* 500:
* description: Internal Server Error
*/
/**
* @swagger
* /measurements:
* get:
* summary: Récupérer toutes les mesures
* description: Récupère toutes les mesures de la base de données.
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{id}:
* get:
* summary: Récupérer une mesure par ID
* description: Récupère une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* get:
* summary: Récupérer une mesure par project ID et order ID
* description: Récupère une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements:
* post:
* summary: Ajouter une nouvelle mesure
* description: Ajoute une nouvelle mesure à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* responses:
* 201:
* description: Mesure ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{id}:
* delete:
* summary: Supprimer une mesure par ID
* description: Supprime une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* delete:
* summary: Supprimer une mesure par project ID et order ID
* description: Supprime une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /uploadmeasurement:
* post:
* summary: Télécharger une mesure avec une image
* description: Télécharge une mesure avec une image pour un projet spécifique.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* timestamp:
* type: string
* format: date-time
* description: Horodatage de la mesure
* temperature:
* type: number
* description: Température mesurée
* humidity:
* type: number
* description: Humidité mesurée
* responses:
* 200:
* description: Mesure téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* id:
* type: integer
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos:
* get:
* summary: Récupérer toutes les vidéos
* description: Récupère toutes les vidéos de la base de données.
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/{id}:
* get:
* summary: Récupérer une vidéo par ID
* description: Récupère une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Une vidéo.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 400:
* description: ID de vidéo invalide.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos:
* post:
* summary: Créer une nouvelle vidéo
* description: Crée une nouvelle vidéo avec les informations fournies.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* name:
* type: string
* resolution:
* type: string
* duration:
* type: number
* responses:
* 200:
* description: Vidéo créée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* video:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* name:
* type: string
* resolution:
* type: string
* duration:
* type: number
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/{id}:
* delete:
* summary: Supprimer une vidéo par ID
* description: Supprime une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Vidéo supprimée avec succès.
* 400:
* description: ID de vidéo invalide.
* 404:
* description: Aucune vidéo trouvée avec cet ID.
* 500:
* description: Erreur serveur.
*/
/**
* @swagger
* /videos/file/{video_id}:
* get:
* summary: Retrieve a video by video ID
* parameters:
* - in: path
* name: video_id
* required: true
* schema:
* type: string
* description: The video ID
* responses:
* 200:
* description: A video file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Video not found
* 400:
* description: Video not yet produced
*/
/**
* @swagger
* /cat:
* get:
* summary: Retrieve a cat video
* responses:
* 200:
* description: A cat video
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Video not found
*/

359
routes/cameraRoutes.js Normal file
View File

@@ -0,0 +1,359 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const serverError = require('../utils/serverError');
const { start } = require('repl');
//const minInterval = 3; // Minutes
//const maxInterval = 60; // Minutes
var defaultCaptureInterval = 5; // minutes
var defaultMaintenance = 0;
var defaultActive = 0; // 0 = pas de capture, 1 = capture en cours
async function initCamera() {
const query = 'SELECT * FROM public.camera WHERE id = $1';
const values = [1];
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la vérification de l\'entrée caméra:', err);
return;
}
if (result.rows.length === 0) {
const insertQuery = `
INSERT INTO public.camera (id, interval, maintenance, active)
VALUES ($1, $2, $3, $4)
`;
const insertValues = [1, defaultCaptureInterval, defaultMaintenance, defaultActive];
db.query(insertQuery, insertValues, (err) => {
if (err) {
console.error('Erreur lors de l\'initialisation de la caméra:', err);
} else {
console.log('Caméra initialisée avec les valeurs par défaut.');
}
});
} else {
console.log('L\'entrée caméra avec l\'ID 1 existe déjà. Aucune initialisation nécessaire.');
}
});
}
async function getCamera() {
// retourner l'état de la caméra
const query = 'SELECT * FROM public.camera WHERE id = $1';
const values = [1];
try {
const result = await db.query(query, values);
if (result.rows.length === 0) {
console.log('Aucune entrée caméra trouvée.');
return null;
} else {
const camera = result.rows[0];
console.log('État de la caméra récupéré avec succès:', camera);
return {
captureInterval: camera.interval,
captureProjectID: camera.active,
captureStatus: camera.active,
maintenance: camera.maintenance
};
}
} catch (err) {
console.error('Erreur lors de la récupération de l\'état de la caméra:', err);
throw err;
}
}
async function printCameraStatus() {
let camera = await getCamera();
console.log('Statut de la caméra:');
console.log('Intervalle de capture:', camera.captureInterval, 'minutes');
console.log('Maintenance:', camera.maintenance === 1 ? 'En cours' : 'Aucune');
console.log('Statut de la capture:', camera.active === 1 ? 'En cours' : 'Arrêté');
console.log('-----------------------------------');
}
async function isCameraOccupied() {
try {
const query = 'SELECT id FROM public.projects WHERE status = $1 LIMIT 1';
const values = [1];
return new Promise((resolve, reject) => {
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la vérification de l\'occupation de la caméra:', err);
reject(err);
} else {
const isOccupied = result.rows.length > 0 ? result.rows[0].id : null;
resolve(isOccupied);
}
});
});
} catch (err) {
console.error('Erreur inattendue lors de la vérification de l\'occupation de la caméra:', err);
}
}
async function getCurrentProject() {
try {
const query = 'SELECT * FROM public.projects WHERE status = $1 LIMIT 1';
const values = [1];
return new Promise((resolve, reject) => {
db.query(query, values, (err, result) => {
if (err) {
console.error('Erreur lors de la récupération du projet en cours:', err);
reject(err);
} else if (result.rows.length === 0) {
console.log('Aucun projet en cours trouvé.');
resolve(null);
} else {
const currentProject = result.rows[0];
console.log('Projet en cours récupéré avec succès:', currentProject);
resolve(currentProject);
}
});
});
} catch (err) {
console.error('Erreur inattendue lors de la récupération du projet en cours:', err);
}
}
async function resetProjectStatus() {
const query = 'UPDATE public.projects SET status = $1 WHERE status = $2';
const values = [2, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la réinitialisation du statut des projets:', err);
} else {
console.log('Statut des projets réinitialisé avec succès.');
}
});
}
async function activateCamera() {
const query = 'UPDATE public.camera SET active = $1 WHERE id = $2';
const values = [1, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de l\'activation de la caméra:', err);
} else {
console.log('Caméra activée avec succès.');
}
});
}
async function deactivateCamera() {
const query = 'UPDATE public.camera SET active = $1 WHERE id = $2';
const values = [0, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la désactivation de la caméra:', err);
} else {
console.log('Caméra désactivée avec succès.');
}
});
}
async function changeProjectStatus(projectId, status) {
try {
const query = 'UPDATE public.projects SET status = $1 WHERE id = $2';
const values = [status, projectId];
await db.query(query, values);
console.log(`Statut du projet ID ${projectId} modifié avec succès à ${status}.`);
} catch (err) {
console.error('Une erreur inattendue s\'est produite lors de la modification du statut du projet:', err);
}
}
async function startup() {
await initCamera();
await printCameraStatus();
}
startup()
.catch(err => {
console.error('Erreur lors de l\'initialisation de la caméra:', err);
});
/**
* @swagger
* /camera/status:
* get:
* summary: Get the current status of the camera
* tags:
* - Camera
* responses:
* 200:
* description: Successfully retrieved the camera status
* content:
* application/json:
* schema:
* type: object
* properties:
* captureInterval:
* type: integer
* description: Capture interval in minutes
* captureProjectID:
* type: integer
* description: ID of the project currently being captured
* captureStatus:
* type: integer
* description: Capture status (0 = stopped, 1 = ongoing)
* maintenance:
* type: integer
* description: Maintenance status (0 = none, 1 = ongoing)
* 500:
* description: Internal server error
*/
router.get('/camera/status', async (req, res) => {
try {
const cameraStatus = await getCamera();
res.status(200).json(cameraStatus);
} catch (err) {
serverError.sendError('Erreur lors de la récupération de l\'état de la caméra:', res, err, 500);
}
});
async function setCameraSettings(interval, maintenance) {
try {
const query = `
UPDATE public.camera
SET interval = $1, maintenance = $2
WHERE id = $3
`;
const values = [interval, maintenance, 1];
db.query(query, values, (err) => {
if (err) {
console.error('Erreur lors de la mise à jour des paramètres de la caméra:', err);
} else {
console.log('Paramètres de la caméra mis à jour avec succès.');
//captureInterval = interval;
//maintenance = maintenance;
}
});
} catch (err) {
console.error('Une erreur inattendue s\'est produite lors de la mise à jour des paramètres de la caméra:', err);
}
}
async function startProcedure(projectId, interval, maintenance) {
if (isNaN(projectId) || isNaN(interval) || isNaN(maintenance)) {
return { error: 'Invalid parameters' };
}
const cameraOccupied = await isCameraOccupied();
if (cameraOccupied) {
return { error: 'Camera is occupied by another project' };
} else {
await activateCamera();
await setCameraSettings(interval, maintenance);
await changeProjectStatus(projectId, 1); // changer le statut du projet en cours à 1 (en cours)
console.log('Procédure de capture démarrée avec succès.');
return { message: 'Capture procedure started successfully' };
}
}
async function stopProcedure() {
var project = await getCurrentProject();
console.log(project);
if (project) {
await resetProjectStatus(); // réinitialiser le statut du projet en cours
await deactivateCamera(); // désactiver la caméra
await changeProjectStatus(project.id, 2); // changer le statut du projet en cours à 2 (terminé)
console.log('Procédure de capture arrêtée avec succès.');
return { message: 'Capture procedure stopped successfully' };
} else {
return { error: 'No project is currently being captured' };
}
}
/**
* @swagger
* /procedure/start/:
* post:
* summary: Start the capture procedure
* tags:
* - Procedure
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* projectId:
* type: integer
* description: ID of the project to start capturing
* interval:
* type: integer
* description: Capture interval in minutes
* maintenance:
* type: integer
* description: Maintenance status (0 = none, 1 = ongoing)
* responses:
* 200:
* description: Successfully started the capture procedure
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* description: Success message
* error:
* type: string
* description: Error message, if any
* 500:
* description: Internal server error
* /procedure/stop/:
* post:
* summary: Stop the capture procedure
* tags:
* - Procedure
* responses:
* 200:
* description: Successfully stopped the capture procedure
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* description: Success message
* error:
* type: string
* description: Error message, if any
* 500:
* description: Internal server error
*/
router.post('/procedure/start/', async (req, res) => {
const { projectId, interval, maintenance } = req.body;
try {
const result = await startProcedure(projectId, interval, maintenance);
res.status(200).json(result);
} catch (err) {
serverError.sendError('Erreur lors du démarrage de la procédure de capture:', res, err, 500);
}
});
router.post('/procedure/stop/', async (req, res) => {
try {
const result = await stopProcedure();
res.status(200).json(result);
} catch (err) {
serverError.sendError('Erreur lors de l\'arrêt de la procédure de capture:', res, err, 500);
}
});
module.exports = router;

View File

@@ -1,27 +1,11 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const path = require('path'); const sharp = require('sharp');
const fs = require('fs'); const fs = require('fs');
const dbTester = require('../test/tester'); const dbTester = require('../test/tester');
const db = require('../db'); const db = require('../db');
const serverError = require('../utils/serverError'); const serverError = require('../utils/serverError');
/**
* @swagger
* /smile:
* get:
* summary: Retrieve a smile image
* responses:
* 200:
* description: A smile image
* content:
* image/jpeg:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
router.get('/smile', (req, res) => { router.get('/smile', (req, res) => {
const imagePath = dbTester.getSmileImage(); const imagePath = dbTester.getSmileImage();
fs.access(imagePath, fs.constants.F_OK, (err) => { fs.access(imagePath, fs.constants.F_OK, (err) => {
@@ -33,42 +17,13 @@ router.get('/smile', (req, res) => {
}); });
}); });
/**
* @swagger
* /images/{projectId}/{orderId}:
* get:
* summary: Retrieve an image by project and order ID
* parameters:
* - in: path
* name: projectId
* required: true
* schema:
* type: string
* description: The project ID
* - in: path
* name: orderId
* required: true
* schema:
* type: string
* description: The order ID
* responses:
* 200:
* description: An image file
* content:
* application/octet-stream:
* schema:
* type: string
* format: binary
* 404:
* description: Image not found
*/
router.get('/images/:projectId/:orderId', (req, res) => { router.get('/images/:projectId/:orderId', (req, res) => {
const projectId = req.params.projectId; const projectId = req.params.projectId;
const orderId = req.params.orderId; const orderId = req.params.orderId;
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2'; const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
db.query(query, [projectId, orderId], (err, results) => { db.query(query, [projectId, orderId], (err, results) => {
if (err) { if (err) {
return serverError.sendError('Error getting image:', res, err); return serverError.sendError('Error getting image:', res, err, 500);
} }
if (results.rows.length === 0) { if (results.rows.length === 0) {
return res.status(404).json({ error: 'Image not found' }); return res.status(404).json({ error: 'Image not found' });
@@ -84,4 +39,77 @@ router.get('/images/:projectId/:orderId', (req, res) => {
}); });
}); });
router.get('/images/:measurementId', (req, res) => {
const measurementId = req.params.measurementId;
const query = 'SELECT path FROM public.measurements WHERE id = $1';
db.query(query, [measurementId], (err, results) => {
if (err) {
return serverError.sendError('Error getting image:', res, err, 500);
}
if (results.rows.length === 0) {
return res.status(404).json({ error: 'Image not found' });
}
const imagePath = results.rows[0].path;
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Image not found:', err);
return res.status(404).json({ error: 'Image not found' });
}
res.download(imagePath);
});
});
});
const getImagePath = async (projectId, orderId) => {
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const result = await db.query(query, [projectId, orderId]);
if (result.rows.length === 0) {
throw new Error('Image not found');
}
return result.rows[0].path;
};
const checkImageExists = (imagePath) => {
return new Promise((resolve, reject) => {
fs.access(imagePath, fs.constants.F_OK, (err) => {
if (err) {
reject(new Error('Image not found'));
} else {
resolve();
}
});
});
};
const resizeImage = async (imagePath) => {
const metadata = await sharp(imagePath).metadata();
const width = Math.floor(metadata.width / 7);
const height = Math.floor(metadata.height / 7);
return sharp(imagePath)
.resize(width, height)
.jpeg({ quality: 65 })
.toBuffer();
};
router.get('/preview/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
try {
const imagePath = await getImagePath(projectId, orderId);
await checkImageExists(imagePath);
const resizedImage = await resizeImage(imagePath);
res.set('Content-Type', 'image/jpeg');
res.send(resizedImage);
} catch (err) {
console.error('Error getting image preview:', err);
if (err.message === 'Image not found') {
return res.status(404).json({ error: 'Image not found' });
}
return res.status(500).json({ error: 'Internal Server Error' });
}
});
module.exports = router; module.exports = router;

View File

@@ -1,282 +1,54 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const database_manager = require('../src/database/database_manager');
const measureManager = require('../src/measure/measureManager');
const serverError = require('../utils/serverError');
/**
* @swagger
* /measurements:
* get:
* summary: Récupérer toutes les mesures
* description: Récupère toutes les mesures de la base de données.
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 500:
* description: Erreur serveur.
*/
router.get('/measurements', (req, res) => { router.get('/measurements', (req, res) => {
const query = 'SELECT * FROM public.measurements'; const measurements = database_manager.measurement.get_all_measurements();
db.query(query, (err, results) => { if (!measurements) {
if (err) { return res.status(404).json({ error: 'No measurements found' });
serverError.sendError('Erreur lors de la récupération des mesures:', res, err);
} }
res.json(results.rows); res.json(measurements);
});
}); });
/**
* @swagger
* /measurements/{id}:
* get:
* summary: Récupérer une mesure par ID
* description: Récupère une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/measurements/:id', (req, res) => { router.get('/measurements/:id', (req, res) => {
const measurementId = req.params.id; const measurement = database_manager.measurement.get_measurement_by_id(req.params.id);
if (!measurementId || isNaN(measurementId)) { if (!measurement) {
return res.status(400).json({ error: 'Invalid measurement ID' }); return res.status(404).json({ error: 'Measurement not found' });
}
res.json(measurement);
});
router.get('/measurements/:projectId/:orderId', async (req, res) => {
const measurement = await database_manager.measurement.get_measurement_by_project_and_order_id(req.params.projectId, req.params.orderId);
if (!measurement) {
return res.status(404).json({ error: 'Measurement not found' });
} }
const query = 'SELECT * FROM public.measurements WHERE id = $1';
db.query(query, [measurementId], (err, results) => {
if (err) {
serverError.sendError('Erreur lors de la récupération de la mesure:', res, err);
}
res.json(results.rows);
});
});
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* get:
* summary: Récupérer une mesure par project ID et order ID
* description: Récupère une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Une mesure.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/measurements/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId;
const orderId = req.params.orderId;
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) {
return res.status(400).json({ error: 'Invalid project ID or order ID' });
}
try {
const measurement = await measureManager.getMeasurement(projectId, orderId);
res.json(measurement); res.json(measurement);
} catch (error) {
serverError.sendError('Error getting measurement:', res, error);
}
}); });
/**
* @swagger
* /measurements:
* post:
* summary: Ajouter une nouvelle mesure
* description: Ajoute une nouvelle mesure à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* timestamp:
* type: string
* format: date-time
* image_path:
* type: string
* temperature:
* type: number
* humidity:
* type: number
* responses:
* 201:
* description: Mesure ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/measurements', (req, res) => { router.post('/measurements', (req, res) => {
const { project_id, timestamp, image_path, temperature, humidity } = req.body; const { projectId, timestamp, imagePath, temperature, humidity, orderId } = req.body;
if (!project_id || !timestamp || !image_path || !temperature || !humidity) { if (!projectId || !timestamp || !imagePath || !temperature || !humidity || !orderId) {
return res.status(400).json({ error: 'All fields are required' }); return res.status(400).json({ error: 'All fields are required' });
} }
const query = 'INSERT INTO public.measurements (project_id, timestamp, image_path, temperature, humidity) VALUES ($1, $2, $3, $4, $5) RETURNING id'; const measurement = database_manager.measurement.add_measurement(projectId, timestamp, imagePath, temperature, humidity, orderId);
db.query(query, [project_id, timestamp, image_path, temperature, humidity], (err, results) => { res.status(201).json(measurement);
if (err) {
serverError.sendError('Erreur lors de l\'ajout de la mesure:', res, err);
}
res.status(201).json({ message: 'Mesure ajoutée avec succès', id: results.rows[0].id });
});
}); });
/**
* @swagger
* /measurements/{id}:
* delete:
* summary: Supprimer une mesure par ID
* description: Supprime une mesure spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la mesure
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de mesure invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/measurements/:id', async (req, res) => { router.delete('/measurements/:id', async (req, res) => {
const measurementId = req.params.id; const measurement = await database_manager.measurement.delete_measurement_by_id(req.params.id);
if (!measurementId || isNaN(measurementId)) { if (!measurement) {
return res.status(400).json({ error: 'Invalid measurement ID' }); return res.status(404).json({ error: 'Measurement not found' });
}
try {
const measurement = await measureManager.deleteMeasurement(measurementId);
res.status(200).json({ message: 'Measurement deleted successfully', id: measurementId });
} catch (error) {
serverError.sendError('Error deleting measurement:', res, error);
} }
res.json({ message: 'Measurement deleted successfully', id: measurement.id });
}); });
/**
* @swagger
* /measurements/{projectId}/{orderId}:
* delete:
* summary: Supprimer une mesure par project ID et order ID
* description: Supprime une mesure spécifique en utilisant le project ID et order ID.
* parameters:
* - in: path
* name: projectId
* schema:
* type: integer
* required: true
* description: ID du projet
* - in: path
* name: orderId
* schema:
* type: integer
* required: true
* description: ID de la commande
* responses:
* 200:
* description: Mesure supprimée avec succès.
* 400:
* description: ID de projet ou de commande invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/measurements/:projectId/:orderId', async (req, res) => { router.delete('/measurements/:projectId/:orderId', async (req, res) => {
const projectId = req.params.projectId; const measurement = await database_manager.measurement.delete_measurement_by_project_and_order_id(req.params.projectId, req.params.orderId);
const orderId = req.params.orderId; if (!measurement) {
if (!projectId || isNaN(projectId) || !orderId || isNaN(orderId)) { return res.status(404).json({ error: 'Measurement not found' });
return res.status(400).json({ error: 'Invalid project ID or order ID' });
}
try {
const measurement = await measureManager.deleteMeasurementByOrderId(projectId, orderId);
res.status(200).json({ message: 'Measurement deleted successfully', id: measurement.id });
} catch (error) {
serverError.sendError('Error deleting measurement:', res, error);
} }
res.json({ message: 'Measurement deleted successfully', id: measurement.id });
}); });
module.exports = router; module.exports = router;

View File

@@ -1,224 +1,90 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const projectManager = require('../src/project/projectManager');
const serverError = require('../utils/serverError'); const serverError = require('../utils/serverError');
const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
/**
* @swagger
* /projects:
* get:
* summary: Récupérer tous les projets
* description: Récupère tous les projets disponibles.
* responses:
* 200:
* description: Une liste de projets.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 500:
* description: Erreur serveur.
*/
router.get('/projects', async (req, res) => { router.get('/projects', async (req, res) => {
try { try {
const projects = await projectManager.getAllProjects(); const projects = await database_manager.project.get_all_projects();
res.json(projects); res.json(projects);
} catch (error) { } catch (error) {
serverError.sendError('Error getting all projects:', res, error); serverError.sendError('Error getting all projects:', res, error, 500);
} }
}); });
/**
* @swagger
* /projects/{id}:
* get:
* summary: Récupérer un projet par ID
* description: Récupère un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Un projet.
* content:
* application/json:
* schema:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id', async (req, res) => { router.get('/projects/:id', async (req, res) => {
const projectId = req.params.id; const projectId = req.params.id;
if (!projectId || isNaN(projectId)) { if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' }); return res.status(400).json({ error: 'Invalid project ID' });
} }
try { try {
const project = await projectManager.getProjectById(projectId); const project = await database_manager.project.get_project_by_id(projectId);
res.json(project); res.json(project);
} catch (error) { } catch (error) {
serverError.sendError('Error getting project by ID:', res, error); serverError.sendError('Error getting project by ID:', res, error, 500);
} }
}); });
/**
* @swagger
* /projects/{id}/videos:
* get:
* summary: Récupérer les vidéos d'un projet par ID
* description: Récupère les vidéos associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id/videos', async (req, res) => { router.get('/projects/:id/videos', async (req, res) => {
const projectId = req.params.id; const projectId = req.params.id;
if (!projectId || isNaN(projectId)) { if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' }); return res.status(400).json({ error: 'Invalid project ID' });
} }
try { try {
const videos = await projectManager.getVideosByProjectId(projectId); const videos = await database_manager.video.get_videos_by_project_id(projectId);
if (videos.length === 0) {
return res.status(404).json({ error: 'No videos found for this project' });
}
res.json(videos); res.json(videos);
} catch (error) { } catch (error) {
serverError.sendError('Error getting videos by project ID:', res, error); serverError.sendError('Error getting videos by project ID:', res, error, 500);
} }
}); });
/**
* @swagger
* /projects/{id}/measurements:
* get:
* summary: Récupérer les mesures d'un projet par ID
* description: Récupère les mesures associées à un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Une liste de mesures.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/projects/:id/measurements', async (req, res) => { router.get('/projects/:id/measurements', async (req, res) => {
const projectId = req.params.id; const projectId = req.params.id;
if (!projectId || isNaN(projectId)) { if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' }); return res.status(400).json({ error: 'Invalid project ID' });
} }
try { try {
const measurements = await projectManager.getMeasurementsByProjectId(projectId); const measurements = await database_manager.measurement.get_measurements_by_project_id(projectId);
if (measurements.length === 0) {
return res.status(404).json({ error: 'No measurements found for this project' });
}
res.json(measurements); res.json(measurements);
} catch (error) { } catch (error) {
serverError.sendError('Error getting measurements by project ID:', res, error); serverError.sendError('Error getting measurements by project ID:', res, error, 500);
} }
}); });
/**
* @swagger
* /projects:
* post:
* summary: Ajouter un nouveau projet
* description: Ajoute un nouveau projet à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* description:
* type: string
* responses:
* 201:
* description: Projet ajouté avec succès.
* 400:
* description: Le nom et la description sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/projects', async (req, res) => { router.post('/projects', async (req, res) => {
const { name, description } = req.body; const { name, description } = req.body;
if (!name || !description) { if (!name || !description) {
return res.status(400).json({ error: 'Name and description are required' }); return res.status(400).json({ error: 'Name and description are required' });
} }
try { try {
const project = await projectManager.createProject(name, description, new Date(), 0); const date = new Date();
projectManager.createProjectDirectory(project.id); const default_status = 0;
const project = await database_manager.project.create_project(name, description, date, default_status);
storage_manager.project.createProjectDirectory(project.id);
res.status(201).json({ message: 'Project added successfully', id: project.id }); res.status(201).json({ message: 'Project added successfully', id: project.id });
} catch (error) { } catch (error) {
serverError.sendError('Error creating project:', res, error); serverError.sendError('Error creating project:', res, error, 500);
} }
}); });
/**
* @swagger
* /projects/{id}:
* delete:
* summary: Supprimer un projet par ID
* description: Supprime un projet spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID du projet
* responses:
* 200:
* description: Projet supprimé avec succès.
* 400:
* description: ID de projet invalide.
* 500:
* description: Erreur serveur.
*/
router.delete('/projects/:id', async (req, res) => { router.delete('/projects/:id', async (req, res) => {
const projectId = req.params.id; const projectId = req.params.id;
if (!projectId || isNaN(projectId)) { if (!projectId || isNaN(projectId)) {
return res.status(400).json({ error: 'Invalid project ID' }); return res.status(400).json({ error: 'Invalid project ID' });
} }
try { try {
projectManager.deleteProjectDirectory(projectId); storage_manager.project.deleteProjectDirectory(projectId);
projectManager.deleteProjectById(projectId); await database_manager.project.delete_project(projectId);
res.status(200).json({ message: 'Project deleted successfully', id: projectId }); res.status(200).json({ message: 'Project deleted successfully', id: projectId });
} catch (error) { } catch (error) {
serverError.sendError('Error deleting project:', res, error); serverError.sendError('Error deleting project:', res, error, 500);
} }
}); });

View File

@@ -1,115 +1,15 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const multer = require('multer'); const multer = require('multer');
const measureManager = require('../src/measure/measureManager'); const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
const serverError = require('../utils/serverError'); const serverError = require('../utils/serverError');
const upload = multer({ storage: multer.memoryStorage() }); const upload = multer({ storage: multer.memoryStorage() });
/**
* @swagger
* /upload:
* post:
* summary: Télécharger une image
* description: Télécharge une image pour un projet et un ordre spécifiques.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* orderId:
* type: integer
* description: ID de la commande
* responses:
* 200:
* description: Image téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/upload', upload.single('image'), async (req, res) => {
const { projectId, orderId } = req.body;
const image = req.file;
if (!image || !projectId || !orderId) {
return res.status(400).json({ error: 'All fields are required' });
}
try {
const imagePath = await measureManager.uploadMeasureImage(image, projectId, orderId);
res.json({ message: 'Image uploaded successfully', path: imagePath });
} catch (error) {
serverError.sendError('Error uploading image:', res, error);
}
});
/**
* @swagger
* /uploadmeasurement:
* post:
* summary: Télécharger une mesure avec une image
* description: Télécharge une mesure avec une image pour un projet spécifique.
* requestBody:
* content:
* multipart/form-data:
* schema:
* type: object
* properties:
* image:
* type: string
* format: binary
* description: Fichier image à télécharger
* projectId:
* type: integer
* description: ID du projet
* timestamp:
* type: string
* format: date-time
* description: Horodatage de la mesure
* temperature:
* type: number
* description: Température mesurée
* humidity:
* type: number
* description: Humidité mesurée
* responses:
* 200:
* description: Mesure téléchargée avec succès.
* content:
* application/json:
* schema:
* type: object
* properties:
* message:
* type: string
* path:
* type: string
* id:
* type: integer
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/uploadmeasurement', upload.single('image'), async (req, res) => { router.post('/uploadmeasurement', upload.single('image'), async (req, res) => {
//afficher le body de la requête
console.log(req.body);
const { projectId, timestamp, temperature, humidity } = req.body; const { projectId, timestamp, temperature, humidity } = req.body;
const image = req.file; const image = req.file;
@@ -118,13 +18,31 @@ router.post('/uploadmeasurement', upload.single('image'), async (req, res) => {
} }
try { try {
const nextOrderId = await measureManager.getNextOrderId(projectId); const nextOrderId = await database_manager.measurement.get_next_order_id(projectId);
const imagePath = await measureManager.uploadMeasureImage(image, projectId, nextOrderId); if (nextOrderId === null) {
const measurement = await measureManager.addMeasureToProject(projectId, timestamp, imagePath, temperature, humidity, nextOrderId); return res.status(404).json({ error: 'Project not found' });
}
// Log types for debugging
console.log('Types:', {
image: typeof image,
projectId: typeof projectId,
nextOrderId: typeof nextOrderId
});
const imagePath = await storage_manager.measurement.upload_measurement_image(image, projectId, nextOrderId);
if (!imagePath) {
return res.status(500).json({ error: 'Failed to upload image' });
}
const measurement = await database_manager.measurement.add_measurement(projectId, timestamp, imagePath, temperature, humidity, nextOrderId);
if (!measurement) {
return res.status(500).json({ error: 'Failed to add measurement' });
}
res.json({ message: 'Measurement uploaded successfully', path: imagePath, id: measurement.id }); res.json({ message: 'Measurement uploaded successfully', path: imagePath, id: measurement.id });
} catch (error) { } catch (error) {
serverError.sendError('Error uploading measurement:', res, error); serverError.sendError('Error uploading measurement:', res, error, 500);
} }
}); });
module.exports = router; module.exports = router;

View File

@@ -1,112 +1,24 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const db = require('../db'); const db = require('../db');
const fs = require('fs');
const rangeParser = require('range-parser');
const serverError = require('../utils/serverError'); const serverError = require('../utils/serverError');
const videoManager = require('../src/video/videoManager'); const videoManager = require('../src/video/videoManager');
const database_manager = require('../src/database/database_manager');
const storage_manager = require('../src/data/storage_manager');
const dbTester = require('../test/tester');
/**
* @swagger
* /videos:
* get:
* summary: Récupérer toutes les vidéos
* description: Récupère toutes les vidéos de la base de données.
* responses:
* 200:
* description: Une liste de vidéos.
* content:
* application/json:
* schema:
* type: array
* items:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 500:
* description: Erreur serveur.
*/
router.get('/videos', (req, res) => { router.get('/videos', (req, res) => {
const query = 'SELECT * FROM public.videos'; const query = 'SELECT * FROM public.videos';
db.query(query, (err, results) => { db.query(query, (err, results) => {
if (err) { if (err) {
serverError.sendError('Erreur lors de la récupération des vidéos:', res, err); serverError.sendError('Erreur lors de la récupération des vidéos:', res, err, 500);
} }
res.json(results.rows); res.json(results.rows);
}); });
}); });
/**
* @swagger
* /videos/{id}:
* get:
* summary: Récupérer une vidéo par ID
* description: Récupère une vidéo spécifique en utilisant son ID.
* parameters:
* - in: path
* name: id
* schema:
* type: integer
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Une vidéo.
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* start_timestamp:
* type: string
* end_timestamp:
* type: string
* image_count:
* type: integer
* resolution:
* type: string
* duration:
* type: number
* fps:
* type: number
* status:
* type: integer
* name:
* type: string
* 400:
* description: ID de vidéo invalide.
* 500:
* description: Erreur serveur.
*/
router.get('/videos/:id', (req, res) => { router.get('/videos/:id', (req, res) => {
const videoId = req.params.id; const videoId = req.params.id;
if (!videoId || isNaN(videoId)) { if (!videoId || isNaN(videoId)) {
@@ -115,119 +27,229 @@ router.get('/videos/:id', (req, res) => {
const query = 'SELECT * FROM public.videos WHERE id = $1'; const query = 'SELECT * FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => { db.query(query, [videoId], (err, results) => {
if (err) { if (err) {
serverError.sendError('Erreur lors de la récupération de la vidéo:', res, err); serverError.sendError('Erreur lors de la récupération de la vidéo:', res, err, 500);
} }
res.json(results.rows); res.json(results.rows);
}); });
}); });
/** router.post('/videos', async (req, res) => {
* @swagger const { project_id, measurement_ids, name, resolution, duration } = req.body;
* /videos: console.log('Creating video:', req.body);
* post: if (!project_id || !measurement_ids || !name || !resolution || !duration) {
* summary: Ajouter une nouvelle vidéo return res.status(400).json({ error: 'Tous les champs sont requis.' });
* description: Ajoute une nouvelle vidéo à la base de données.
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* project_id:
* type: integer
* measurement_ids:
* type: string
* video_path:
* type: string
* duration:
* type: number
* resolution:
* type: string
* name:
* type: string
* responses:
* 201:
* description: Vidéo ajoutée avec succès.
* 400:
* description: Tous les champs sont requis.
* 500:
* description: Erreur serveur.
*/
router.post('/videos', (req, res) => {
const { project_id, measurement_ids, video_path, duration, resolution, name } = req.body;
if (!project_id || !measurement_ids || !video_path || !duration || !resolution || !name) {
return res.status(400).json({ error: 'All fields are required' });
} }
const list_ids = measurement_ids.split(','); console.log('Creating video with measurements:', measurement_ids);
const image_count = list_ids.length;
const videoPath = '/videos/' + name + '.mp4';
const query_first = 'SELECT timestamp FROM public.measurements WHERE id = $1'; try {
const query_last = 'SELECT timestamp FROM public.measurements WHERE id = $1'; const videoId = await videoManager.createVideoProject(project_id, measurement_ids, name, resolution, duration);
db.query(query_first, [list_ids[0]], (err, results) => { // Start rendering the video immediately after creation
if (err) { const result = await db.query(
serverError.sendError('Erreur lors de la récupération du timestamp de la première image:', res, err); 'SELECT measurement_ids, project_id, duration FROM public.videos WHERE id = $1',
[videoId]
);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Vidéo non trouvée' });
} }
const start_timestamp = results.rows[0].timestamp;
db.query(query_last, [list_ids[image_count - 1]], (err, results) => { const { duration: videoDuration, measurement_ids: videoMeasurementIds, project_id: videoProjectId } = result.rows[0];
if (err) { const pathList = await storage_manager.measurement.get_path_list(videoMeasurementIds, project_id);
serverError.sendError('Erreur lors de la récupération du timestamp de la dernière image:', res, err); if (!pathList || pathList.length === 0) {
return res.status(404).json({ error: 'Aucun chemin trouvé pour les mesures' });
} }
const end_timestamp = results.rows[0].timestamp;
const fps = image_count / duration;
const query = 'INSERT INTO public.videos (project_id, measurement_ids, video_path, start_timestamp, end_timestamp, image_count, resolution, duration, fps, status, name) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING id'; // parser la résolution (ex: 1920x1080)
db.query(query, [project_id, measurement_ids, videoPath, start_timestamp, end_timestamp, image_count, resolution, duration, fps, 0, name], (err, results) => { const [res_width, res_height] = resolution.split('x').map(Number);
if (err) { if (isNaN(res_width) || isNaN(res_height)) {
serverError.sendError('Erreur lors de l\'ajout de la vidéo:', res, err); return res.status(400).json({ error: 'Invalid resolution format. Use WIDTHxHEIGHT (e.g., 1920x1080)' });
} }
res.status(201).json({ message: 'Vidéo ajoutée avec succès', id: results.rows[0].id });
}); // Start background processing
}); videoManager.createVideoWithList(videoProjectId, pathList, videoDuration, videoId, res_width, res_height)
}); .then(videoFile => {
console.log('Rendu vidéo terminé:', videoFile);
return database_manager.video.update_video_file_path_by_id(videoId, videoFile);
})
.catch(error => {
console.error('Échec du rendu vidéo:', error);
}); });
/** // Immediate response
* @swagger res.json({
* /videos/{id}: message: 'Vidéo créée avec succès et le rendu a démarré',
* delete: id: videoId
* summary: Supprimer une vidéo par ID });
* description: Supprime une vidéo spécifique en utilisant son ID.
* parameters: } catch (err) {
* - in: path console.error('Erreur lors de la création de la vidéo:', err);
* name: id res.status(500).json({ error: 'Erreur lors de la création de la vidéo' });
* schema: }
* type: integer });
* required: true
* description: ID de la vidéo
* responses:
* 200:
* description: Vidéo supprimée avec succès.
* 400:
* description: ID de vidéo invalide.
* 404:
* description: Aucune vidéo trouvée avec cet ID.
* 500:
* description: Erreur serveur.
*/
router.delete('/videos/:id', (req, res) => { router.delete('/videos/:id', (req, res) => {
const videoId = req.params.id; const videoId = req.params.id;
if (!videoId || isNaN(videoId)) { if (!videoId || isNaN(videoId)) {
return res.status(400).json({ error: 'Invalid video ID' }); return res.status(400).json({ error: 'Invalid video ID' });
} }
const query = 'DELETE FROM public.videos WHERE id = $1 RETURNING id';
const query = 'SELECT video_file FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => { db.query(query, [videoId], (err, results) => {
if (err) { if (err) {
serverError.sendError('Erreur lors de la suppression de la vidéo:', res, err); return serverError.sendError('Error getting video:', res, err, 500);
} }
if (results.rowCount === 0) { if (results.rows.length === 0) {
return res.status(404).json({ error: 'Aucune vidéo trouvée avec cet ID.' }); return res.status(404).json({ error: 'Video not found' });
} }
res.status(200).json({ message: 'Vidéo supprimée avec succès', id: videoId });
const videoFile = results.rows[0].video_file;
console.log('Deleting video file:', videoFile);
if(videoFile==null){
console.log('No video file to delete');
videoManager.deleteVideoProject(videoId).then(() => {
res.json({ message: 'Vidéo supprimée avec succès' });
}).catch(err => {
console.error('Erreur lors de la suppression de la vidéo:', err);
res.status(500).json({ error: 'Erreur lors de la suppression de la vidéo' });
});
} else {
fs.unlink(videoFile, (err) => {
if (err) {
console.error('Error deleting video file:', err);
return res.status(500).json({ error: 'Error deleting video file' });
}
videoManager.deleteVideoProject(videoId).then(() => {
res.json({ message: 'Vidéo supprimée avec succès' });
}).catch(err => {
console.error('Erreur lors de la suppression de la vidéo:', err);
res.status(500).json({ error: 'Erreur lors de la suppression de la vidéo' });
});
});
}
});
});
router.get('/videos/file/:video_id', (req, res) => {
const videoId = req.params.video_id;
const query = 'SELECT video_file, status FROM public.videos WHERE id = $1';
db.query(query, [videoId], (err, results) => {
if (err) {
console.error('Error getting video:', err);
return serveFallbackVideo(res);
}
if (results.rows.length === 0) {
console.error('Video not found');
return serveFallbackVideo(res);
}
const video = results.rows[0];
if (video.status === 0) {
return res.status(400).json({ error: 'Video not yet produced' });
}
let videoPath = video.video_file;
if (!videoPath) {
console.error('Video file path is null or undefined');
videoPath = dbTester.getCatVideo();
}
// Check if the video file exists
fs.access(videoPath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Video file not found:', err);
videoPath = dbTester.getCatVideo();
}
const stat = fs.statSync(videoPath);
const fileSize = stat.size;
const range = req.headers.range;
if (range) {
const parts = rangeParser(fileSize, range);
const start = parts[0].start;
const end = parts[0].end;
const chunksize = (end - start) + 1;
const file = fs.createReadStream(videoPath, { start, end });
const head = {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'video/mp4',
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
'Content-Length': fileSize,
'Content-Type': 'video/mp4',
};
res.writeHead(200, head);
fs.createReadStream(videoPath).pipe(res);
}
});
});
});
function serveFallbackVideo() {
return dbTester.getCatVideo();
}
router.get('/videos/progress/:video_id', async (req, res) => {
try {
const result = await db.query(`
SELECT
progress,
EXTRACT(EPOCH FROM (NOW() - started_at)) as elapsed,
eta,
status
FROM public.videos
WHERE id = $1
`, [req.params.video_id]);
if (result.rows.length === 0) {
return res.status(404).json({ error: 'Vidéo non trouvée' });
}
const video = result.rows[0];
res.json({
progress: video.progress,
elapsed: video.elapsed,
eta: video.eta,
status: this.getStatusLabel(video.status)
});
} catch (error) {
console.error(error);
res.status(500).json({ error: 'Erreur de récupération' });
}
});
// function getStatusLabel(status) {
// const statusMap = {
// 0: 'En attente',
// 1: 'Terminé',
// 2: 'Échec',
// 3: 'En cours'
// };
// return statusMap[status] || 'Inconnu';
// }
router.get('/cat', (_, res) => {
const videoPath = dbTester.getCatVideo();
fs.access(videoPath, fs.constants.F_OK, (err) => {
if (err) {
console.error('Video not found:', err);
return res.status(404).json({ error: 'Video not found' });
}
res.download(videoPath);
}); });
}); });

BIN
sample/cat.mp4 Normal file

Binary file not shown.

68
server_local.js Normal file
View File

@@ -0,0 +1,68 @@
const devlock = require('./devlock.js');
devlock.is_dev = true; // Set to true for development mode
// server.js version locale
const express = require('express');
const cors = require('cors');
const app = express();
const port = 3000;
// Middleware pour gérer les requêtes JSON
app.use(express.json());
// Cors accès à tout
app.use(cors({
origin: ['http://127.0.0.1:5500', 'http://localhost:5500', 'http://localhost:3000'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type'],
credentials: true,
}));
// Importer les routes
const apiRoutes = require('./api');
app.use('/api', apiRoutes);
// Swagger dependencies
const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');
// Configuration de Swagger
const swaggerOptions = {
definition: {
openapi: '3.0.0',
info: {
title: 'API Documentation',
version: '1.0.0',
description: 'Documentation de l\'API avec Swagger',
},
servers: [
{
url: 'https://timelapse.kerboul.me/api',
},
{
url: 'http://localhost:3000/api',
}
],
},
apis: ['./routes/*.js'], // Prend en compte tous les fichiers de routes pour générer la documentation
};
// Initialisation de swagger-jsdoc
const swaggerDocs = swaggerJsdoc(swaggerOptions);
// Route Swagger UI
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocs));
// Route de base pour tester le serveur
app.get('/', (req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
res.send('Bienvenue sur mon API Node.js!');
});
// Démarrer le serveur
app.listen(port, () => {
console.log(`[SERVER] Serveur démarré sur http://localhost:${port}`);
console.log(`[SERVER] Swagger documentation disponible sur http://localhost:${port}/api-docs`);
});

View File

@@ -1,18 +1,14 @@
import db from '../../db.js'; const db = require('../../db.js');
import path from 'path'; const storage_manager = require('./storage_manager.js');
import storageManager from '../data/storageManager.js'; const fs = require('fs');
import fs from 'fs';
let localCounter = 0; let localCounter = 0;
async function checkAndRemoveInvalidEntries() { async function checkAndRemoveInvalidEntries() {
localCounter = 0; localCounter = 0;
console.log('[INFO] Vérification et suppression des entrées invalides...');
try { try {
const measurementsRes = await db.query('SELECT id, path FROM measurements'); const measurementsRes = await db.query('SELECT id, path FROM measurements');
//console.log('Fetched measurements:', measurementsRes.rows);
for (const row of measurementsRes.rows) { for (const row of measurementsRes.rows) {
//console.log('Checking file path:', row.path);
if (!fs.existsSync(row.path)) { if (!fs.existsSync(row.path)) {
await db.query('DELETE FROM measurements WHERE id = $1', [row.id]); await db.query('DELETE FROM measurements WHERE id = $1', [row.id]);
console.log(`Deleted invalid measurement entry with id: ${row.id}`); console.log(`Deleted invalid measurement entry with id: ${row.id}`);
@@ -20,22 +16,18 @@ async function checkAndRemoveInvalidEntries() {
} }
} }
// Scan all images in storage const allImages = await storage_manager.scanAllImages();
const allImages = await storageManager.scanAllImages();
//console.log('Scanned all images:', allImages);
for (const imagePath of allImages) { for (const imagePath of allImages) {
const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]); const entryRes = await db.query('SELECT id FROM measurements WHERE path = $1', [imagePath]);
if (entryRes.rows.length === 0) { if (entryRes.rows.length === 0) {
// Remove the file if the entry does not exist
fs.unlinkSync(imagePath); fs.unlinkSync(imagePath);
console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`); console.log(`Deleted file at path: ${imagePath} as its database entry does not exist`);
localCounter++; // Increment counter if entry is deleted localCounter++;
} }
} }
if (localCounter > 0) { if (localCounter > 0) {
console.log(`[INFO] ${localCounter} entrées ont été modifiées`); console.log(`[INFO] ${localCounter} entrées ont été modifiées`);
} else { localCounter = 0; // Reset the counter after logging
console.log('[INFO] Aucune entrée n\'a été modifiée.');
} }
} catch (err) { } catch (err) {
console.error('Error checking and removing invalid entries:', err); console.error('Error checking and removing invalid entries:', err);

View File

@@ -1,87 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
const PROJECTS_DIR = path.join('.');
async function createFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(projectDir, { recursive: true });
} else {
throw error;
}
}
return projectDir;
}
async function deleteFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
await fs.rm(projectDir, { recursive: true, force: true });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
async function scanAllImages(dir = 'storage') {
const projectDir = path.join(PROJECTS_DIR, dir);
let results = [];
async function scanDirectory(directory) {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await scanDirectory(filePath);
} else if (file.endsWith('.jpg')) {
results.push(filePath);
}
}
}
await scanDirectory(projectDir);
return results;
}
async function saveFile(filePath, content) {
if (Buffer.isBuffer(content)) {
await fs.writeFile(filePath, content);
} else {
throw new Error('Content must be a buffer');
}
}
async function getFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
return await fs.readFile(filePath);
}
async function deleteFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(filePath); // Vérifie si le fichier existe
await fs.rm(filePath); // Supprime le fichier
return `File ${filePath} deleted successfully.`;
} catch (error) {
if (error.code === 'ENOENT') {
return `File ${filePath} does not exist.`;
} else {
throw error; // Relance l'erreur si ce n'est pas une erreur de fichier introuvable
}
}
}
module.exports = {
createFolder,
deleteFolder,
scanAllImages,
saveFile,
getFile,
deleteFile
};

196
src/data/storage_manager.js Normal file
View File

@@ -0,0 +1,196 @@
const fs = require('fs').promises;
const path = require('path');
const PROJECTS_DIR = path.join('.');
const database_manager = require('../database/database_manager.js');
async function createFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(projectDir, { recursive: true });
} else {
throw error;
}
}
return projectDir;
}
async function deleteFolder(name) {
const projectDir = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(projectDir);
await fs.rm(projectDir, { recursive: true, force: true });
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}
}
async function scanAllImages(dir = 'storage') {
const projectDir = path.join(PROJECTS_DIR, dir);
let results = [];
// check if the directory exists and create it if not
try {
await fs.access(projectDir);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(projectDir, { recursive: true });
} else {
throw error;
}
}
async function scanDirectory(directory) {
const files = await fs.readdir(directory);
for (const file of files) {
const filePath = path.join(directory, file);
const stat = await fs.stat(filePath);
if (stat.isDirectory()) {
await scanDirectory(filePath);
} else if (file.endsWith('.jpg')) {
results.push(filePath);
}
}
}
await scanDirectory(projectDir);
return results;
}
async function saveFile(filePath, content) {
let Buffer=Buffer.from(content, 'base64');
if (Buffer.isBuffer(content)) {
await fs.writeFile(filePath, content);
} else {
throw new Error('Content must be a buffer');
}
}
async function getFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
return await fs.readFile(filePath);
}
async function deleteFile(name) {
const filePath = path.join(PROJECTS_DIR, `${name}`);
try {
await fs.access(filePath); // Vérifie si le fichier existe
await fs.rm(filePath); // Supprime le fichier
return `File ${filePath} deleted successfully.`;
} catch (error) {
if (error.code === 'ENOENT') {
return `File ${filePath} does not exist.`;
} else {
throw error; // Relance l'erreur si ce n'est pas une erreur de fichier introuvable
}
}
}
async function handleFileOperation(operation, ...args) {
try {
return await operation(...args);
} catch (error) {
console.error(`[FILE OPERATION ERROR] ${error.message}`);
throw error;
}
}
const project = {
createProjectDirectory: async function (projectId) {
const projectPath = `${projectId}`;
await handleFileOperation(createFolder, projectPath);
await handleFileOperation(createFolder, `${projectPath}/images`);
await handleFileOperation(createFolder, `${projectPath}/videos`);
console.log("[FILE] createProjectDirectory : " + projectPath);
},
deleteProjectDirectory: async function (projectId) {
const projectPath = `${projectId}`;
await handleFileOperation(deleteFolder, projectPath);
console.log("[FILE] deleteProjectDirectory : " + projectPath);
}
};
const measurement = {
get_measurement_image: async function (projectId, orderId) {
const projectPath = `${projectId}`;
const imagePath = `${projectPath}/images/${orderId}.jpg`;
console.log("[FILE] get_measurement_image : " + imagePath);
return await handleFileOperation(getFile, imagePath);
},
upload_measurement_image: async function (image, projectId, orderId) {
const projectPath = `${projectId}`;
const imagePath = `${projectPath}/images/${orderId}.jpg`;
console.log("[FILE] upload_measurement_image : " + imagePath);
await handleFileOperation(saveFile, imagePath, image.buffer);
return imagePath;
},
get_path_from_id: async function (projectId, orderId) {
const query = database_manager.measurement.get_measurement_by_project_and_order_id(projectId, orderId);
return query.path;
},
get_path_list: async function (IdList, projectId) {
let parsedIdList;
try {
parsedIdList = JSON.parse(IdList);
} catch (e) {
console.error("Error parsing IdList:", e);
return [];
}
const pathList = [];
for (const orderId of parsedIdList) {
const path = await this.get_path_from_id(projectId, orderId);
pathList.push(path);
}
return pathList;
},
}
const video = {
get_video: async function (projectId, orderId) {
const projectPath = `${projectId}`;
const videoPath = `${projectPath}/videos/${orderId}.mp4`;
console.log("[FILE] get_video : " + videoPath);
return await handleFileOperation(getFile, videoPath);
},
delete_video: async function (videoId) {
const query = database_manager.video.get_video_by_id(videoId);
const videoPath = query.video_file;
console.log("[FILE] delete_video : " + videoPath);
return await handleFileOperation(deleteFile, videoPath);
},
delete_unfinished_videos: async function () {
const unfinishedVideos = await database_manager.video.get_unfinished_videos();
for (const video of unfinishedVideos) {
try {
await this.delete_video(video.id);
console.log(`Deleted unfinished video with id: ${video.id}`);
} catch (error) {
console.error(`Error deleting unfinished video with id: ${video.id}`, error);
}
}
}
}
module.exports = {
createFolder,
deleteFolder,
scanAllImages,
saveFile,
getFile,
deleteFile,
project,
measurement,
video
};

View File

@@ -0,0 +1,285 @@
const db = require('../../db.js');
// Fonctions de gestion de la base de données interne
async function create_database() {
const queries = [
`CREATE TABLE IF NOT EXISTS projects (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
start_date DATE,
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3]))
);`,
`ALTER TABLE projects OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS measurements (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
timestamp TIMESTAMP NOT NULL,
path VARCHAR(255),
temperature DOUBLE PRECISION,
humidity DOUBLE PRECISION,
order_id INTEGER NOT NULL,
CONSTRAINT unique_project_photo_order UNIQUE (project_id, order_id)
);`,
`ALTER TABLE measurements OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS videos (
id SERIAL PRIMARY KEY,
project_id INTEGER REFERENCES projects ON DELETE CASCADE,
measurement_ids TEXT NOT NULL,
video_file VARCHAR(255),
resolution VARCHAR(255),
duration INTEGER,
status INTEGER NOT NULL CHECK (status = ANY (ARRAY [0, 1, 2, 3])),
name VARCHAR(255),
progress DOUBLE PRECISION,
started_at TIMESTAMP,
updated_at TIMESTAMP,
eta DOUBLE PRECISION
);`,
`ALTER TABLE videos OWNER TO timelapse;`,
`CREATE TABLE IF NOT EXISTS camera (
id SERIAL PRIMARY KEY,
interval INTEGER NOT NULL,
maintenance INTEGER NOT NULL,
active INTEGER DEFAULT 0 NOT NULL
);`,
`ALTER TABLE camera OWNER TO timelapse;`
];
try {
for (const query of queries) {
await db.query(query);
}
console.log('Database tables created or verified successfully.');
} catch (err) {
console.error('Error creating database tables:', err);
throw err;
}
}
async function check_database_existence() {
const query = `
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name IN ('projects', 'measurements', 'videos', 'camera');
`;
try {
const result = await db.query(query);
const existingTables = result.rows.map(row => row.table_name);
const requiredTables = ['projects', 'measurements', 'videos', 'camera'];
const missingTables = requiredTables.filter(table => !existingTables.includes(table));
if (missingTables.length > 0) {
console.error('Missing or improperly constructed tables:', missingTables);
throw new Error(`The following tables are missing or not properly constructed: ${missingTables.join(', ')}`);
} else {
console.log('All required tables exist and are properly constructed.');
}
} catch (err) {
console.error('Error checking database tables:', err);
throw err;
}
}
async function delete_database() {
const queries = [
`DROP TABLE IF EXISTS videos;`,
`DROP TABLE IF EXISTS measurements;`,
`DROP TABLE IF EXISTS projects;`,
`DROP TABLE IF EXISTS camera;`
];
try {
for (const query of queries) {
await db.query(query);
}
console.log('Database tables deleted successfully.');
} catch (err) {
console.error('Error deleting database tables:', err);
throw err;
}
}
async function init_function() {
try {
await check_database_existence();
} catch (err) {
console.error('Database check failed:', err);
try {
await delete_database();
await create_database();
console.log('Database initialized successfully.');
} catch (err) {
console.error('Error initializing database:', err);
throw err;
}
} finally {
console.log('Database initialization process completed.');
}
}
init_function()
.then(() => console.log('Database initialization completed.'))
.catch(err => console.error('Error during database initialization:', err));
// Fonctions pour les projets
function handleDatabaseOperation(operation) {
return async (...args) => {
try {
return await operation(...args);
} catch (err) {
console.error(`Error during database operation: ${operation.name}`, err);
throw err;
}
};
}
const project = {
get_all_projects: handleDatabaseOperation(async () => {
const query = `SELECT * FROM projects;`;
return (await db.query(query)).rows;
}),
get_project_by_id: handleDatabaseOperation(async (id) => {
const query = `SELECT * FROM projects WHERE id = $1;`;
return (await db.query(query, [id])).rows[0];
}),
create_project: handleDatabaseOperation(async (name, description, start_date, status) => {
const query = `INSERT INTO projects (name, description, start_date, status) VALUES ($1, $2, $3, $4) RETURNING *;`;
return (await db.query(query, [name, description, start_date, status])).rows[0];
}),
edit_project_by_id: handleDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE projects SET ${fields} WHERE id = $1 RETURNING *;`;
return (await db.query(query, values)).rows[0];
}),
delete_project_by_id: handleDatabaseOperation(async (id) => {
const query = `DELETE FROM projects WHERE id = $1;`;
await db.query(query, [id]);
})
};
const measurement = {
get_all_measurements: handleDatabaseOperation(async () => {
const query = `SELECT * FROM measurements;`;
return (await db.query(query)).rows;
}),
get_measurement_by_id: handleDatabaseOperation(async (id) => {
const query = `SELECT * FROM measurements WHERE id = $1;`;
return (await db.query(query, [id])).rows[0];
}),
get_measurement_by_project_and_order_id: handleDatabaseOperation(async (project_id, order_id) => {
const query = `SELECT * FROM measurements WHERE project_id = $1 AND order_id = $2;`;
return (await db.query(query, [project_id, order_id])).rows[0];
}),
get_measurements_by_project_id: handleDatabaseOperation(async (project_id) => {
const query = `SELECT * FROM measurements WHERE project_id = $1;`;
return (await db.query(query, [project_id])).rows;
}),
create_measurement: handleDatabaseOperation(async (project_id, timestamp, path, temperature, humidity, order_id) => {
const query = `INSERT INTO measurements (project_id, timestamp, path, temperature, humidity, order_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *;`;
return (await db.query(query, [project_id, timestamp, path, temperature, humidity, order_id])).rows[0];
}),
edit_measurement_by_id: handleDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE measurements SET ${fields} WHERE id = $1 RETURNING *;`;
return (await db.query(query, values)).rows[0];
}),
edit_measurement_by_project_and_order_id: handleDatabaseOperation(async (project_id, order_id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 3}`).join(', ');
const values = [project_id, order_id, ...Object.values(updates)];
const query = `UPDATE measurements SET ${fields} WHERE project_id = $1 AND order_id = $2 RETURNING *;`;
return (await db.query(query, values)).rows[0];
}),
delete_measurement_by_id: handleDatabaseOperation(async (id) => {
const query = `DELETE FROM measurements WHERE id = $1;`;
await db.query(query, [id]);
}),
get_next_order_id: handleDatabaseOperation(async (project_id) => {
const query = `SELECT COALESCE(MAX(order_id), 0) + 1 AS next_order_id FROM measurements WHERE project_id = $1;`;
const result = await db.query(query, [project_id]);
return result.rows[0].next_order_id;
})
};
const video = {
get_all_videos: handleDatabaseOperation(async () => {
const query = `SELECT * FROM videos;`;
return (await db.query(query)).rows;
}),
get_video_by_id: handleDatabaseOperation(async (id) => {
const query = `SELECT * FROM videos WHERE id = $1;`;
return (await db.query(query, [id])).rows[0];
}),
get_videos_by_project_id: handleDatabaseOperation(async (project_id) => {
const query = `SELECT * FROM videos WHERE project_id = $1;`;
return (await db.query(query, [project_id])).rows;
}),
create_video: handleDatabaseOperation(async (projectId, measurementIds, name, resolution, duration, status = 0) => {
const query = `INSERT INTO public.videos (project_id, measurement_ids, name, resolution, duration, status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id;`;
const values = [projectId, measurementIds, name, resolution, duration, status];
return (await db.query(query, values)).rows[0];
}),
edit_video_by_id: handleDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE videos SET ${fields} WHERE id = $1 RETURNING *;`;
return (await db.query(query, values)).rows[0];
}),
update_video_file_path_by_id: handleDatabaseOperation(async (id, video_file) => {
const query = `UPDATE videos SET video_file = $1 WHERE id = $2 RETURNING *;`;
return (await db.query(query, [video_file, id])).rows[0];
}
),
delete_video_by_id: handleDatabaseOperation(async (id) => {
const query = `DELETE FROM videos WHERE id = $1;`;
await db.query(query, [id]);
}),
get_unfinished_videos: handleDatabaseOperation(async () => {
// récupérer liste des vidéos dont le status est = 0, 2 ou 3
const query = `SELECT * FROM videos WHERE status IN (0, 2, 3);`;
return (await db.query(query)).rows;
}),
};
const camera = {
get_camera: handleDatabaseOperation(async () => {
const query = `SELECT * FROM camera;`;
return (await db.query(query)).rows;
}),
edit_camera: handleDatabaseOperation(async (id, updates) => {
const fields = Object.keys(updates).map((key, index) => `${key} = $${index + 2}`).join(', ');
const values = [id, ...Object.values(updates)];
const query = `UPDATE camera SET ${fields} WHERE id = $1 RETURNING *;`;
return (await db.query(query, values)).rows[0];
}),
delete_camera: handleDatabaseOperation(async (id) => {
const query = `DELETE FROM camera WHERE id = $1;`;
await db.query(query, [id]);
})
};
// Export des modules
module.exports = {
project,
measurement,
video,
camera
};

View File

@@ -1,97 +0,0 @@
import db from '../../db.js';
import path from 'path';
import storageManager from '../data/storageManager.js';
async function uploadMeasureImage(image, projectId, orderId) {
const projectDir = storageManager.createFolder('./storage/' + projectId.toString());
const imagesDir = storageManager.createFolder(path.join(projectDir, 'images'));
var imagePath = path.join(imagesDir, `${orderId}.jpg`);
storageManager.saveFile(imagePath, image.buffer);
console.log("[FILE] uploadMeasureImage - Image saved to: " + imagePath);
return imagePath;
}
async function getMeasureImage(projectId, orderId) {
const projectPath = `${projectId}`;
const imagePath = `${projectPath}/${orderId}.jpg`;
console.log("[FILE] getMeasureImage - Image path: " + imagePath);
return storageManager.getFile(imagePath);
}
async function getNextOrderId(projectId) {
const query = 'SELECT MAX(order_id) FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getNextOrderId - Max order_id: " + res.rows[0].max);
return res.rows[0].max + 1;
}
async function addMeasureToProject(projectId, orderId, timestamp, path, temperature, humidity) {
const query = 'INSERT INTO public.measurements (project_id, timestamp, path, temperature, humidity, order_id) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *';
const values = [projectId, orderId, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function getMeasurements(projectId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
return res.rows;
}
async function getMeasurement(projectId, orderId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const values = [projectId, orderId];
const res = await db.query(query, values);
return res.rows[0];
}
async function getMeasurementById(id) {
const query = 'SELECT * FROM public.measurements WHERE id = $1';
const values = [id];
const res = await db.query(query, values);
return res.rows[0];
}
async function updateMeasurement(projectId, orderId, timestamp, path, temperature, humidity) {
const query = 'UPDATE public.measurements SET timestamp = $3, path = $4, temperature = $5, humidity = $6 WHERE project_id = $1 AND order_id = $2 RETURNING *';
const values = [projectId, orderId, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function updateMeasurementById(id, timestamp, path, temperature, humidity) {
const query = 'UPDATE public.measurements SET timestamp = $2, path = $3, temperature = $4, humidity = $5 WHERE id = $1 RETURNING *';
const values = [id, timestamp, path, temperature, humidity];
const res = await db.query(query, values);
return res.rows[0];
}
async function deleteMeasurement(id) {
const query = 'DELETE FROM public.measurements WHERE id = $1';
const values = [id];
const res = await db.query(query, values);
return res.rows[0];
}
async function getPathFromIds(projectId, orderId) {
const query = 'SELECT path FROM public.measurements WHERE project_id = $1 AND order_id = $2';
const values = [projectId, orderId];
const res = await db.query(query, values);
return res.rows[0].path;
}
export {
uploadMeasureImage,
addMeasureToProject,
getNextOrderId,
getMeasurements,
getMeasurement,
updateMeasurement,
deleteMeasurement,
getMeasureImage,
getMeasurementById,
updateMeasurementById,
getPathFromIds
}

View File

@@ -1,82 +0,0 @@
import storageManager from '../data/storageManager.js';
import db from '../../db.js';
function createProjectDirectory(projectId) {
const projectPath = `${projectId}`;
storageManager.createFolder(projectPath);
storageManager.createFolder(`${projectPath}/images`);
storageManager.createFolder(`${projectPath}/videos`);
console.log("[FILE] createProjectDirectory : " + projectPath);
}
function deleteProjectDirectory(projectId) {
const projectPath = `${projectId}`;
storageManager.deleteFolder(projectPath);
console.log("[FILE] deleteProjectDirectory : " + projectPath);
}
async function getAllProjects() {
const query = 'SELECT * FROM public.projects';
const res = await db.query(query);
console.log("[DB] getAllProjects : ", res.rows);
return res.rows;
}
async function getProjectById(projectId) {
const query = 'SELECT * FROM public.projects WHERE id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getProjectById : ", res.rows[0]);
return res.rows[0];
}
async function createProject(name, description, start_date, status) {
const query = 'INSERT INTO public.projects (name, description, start_date, status) VALUES ($1, $2, $3, $4) RETURNING *';
const values = [name, description, start_date, status];
const res = await db.query(query, values);
console.log("[DB] createProject : ", res.rows[0]);
return res.rows[0];
}
async function editProjectById(projectID, name, description, startDate, status) {
const query = 'UPDATE public.projects SET name = $1, description = $2, start_date = $3, status = $4 WHERE id = $5 RETURNING *';
const values = [name, description, startDate, status, projectID];
const res = await db.query(query, values);
console.log("[DB] editProjectById : ", res.rows[0]);
return res.rows[0];
}
async function deleteProjectById(projectId) {
const query = 'DELETE FROM public.projects WHERE id = $1';
const values = [projectId];
console.log("[DB] deleteProjectById : ", values);
await db.query(query, values);
}
async function getVideosByProjectId(projectId) {
const query = 'SELECT * FROM public.videos WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getVideosByProjectId : ", res.rows);
return res.rows;
}
async function getMeasurementsByProjectId(projectId) {
const query = 'SELECT * FROM public.measurements WHERE project_id = $1';
const values = [projectId];
const res = await db.query(query, values);
console.log("[DB] getMeasurementsByProjectId : ", res.rows);
return res.rows;
}
export {
createProjectDirectory,
deleteProjectDirectory,
getAllProjects,
getProjectById,
createProject,
editProjectById,
deleteProjectById,
getVideosByProjectId,
getMeasurementsByProjectId
};

View File

@@ -1,111 +1,174 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { execSync } = require('child_process'); const { spawn } = require('child_process');
const serverError = require('../../utils/serverError'); const database_manager = require('../database/database_manager');
const db = require('../../db');
const storageManager = require('../data/storageManager');
const measureManager = require('../measure/measureManager');
const PROJECTS_DIR = path.join('.'); const PROJECTS_DIR = path.join('.');
async function createVideoWithList(projectId, pathList) { async function createVideoWithList(projectId, pathList, duration, videoId, res_width, res_height) {
//pathList étant la liste des chemins déjà triés
const tempFile = path.join('temp.txt'); const tempFile = path.join('temp.txt');
try { let ffmpegProcess;
// Trouver tous les fichiers image pour le projet donné let cleanupDone = false;
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`);
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images');
console.log('dir:', dir);
const images = pathList;
console.log('images:', images);
// Trier les images numériquement try {
const sortedImages = images.sort((a, b) => { // Configuration des chemins
const workdir = path.join(PROJECTS_DIR, 'storage', projectId.toString());
if (!fs.existsSync(workdir)) {
fs.mkdirSync(workdir, { recursive: true });
}
// Tri des images
const sortedImages = pathList.sort((a, b) => {
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10); const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10); const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
return numA - numB; return numA - numB;
}); });
// En déduire l'id de la première et dernière image utilisée // Création du fichier temporaire
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
console.log('firstImageId:', firstImageId);
console.log('lastImageId:', lastImageId);
// Créer un fichier temporaire pour la liste des images
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n')); fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
const frameRate = 10; // Calcul des paramètres vidéo
const totalFrames = sortedImages.length;
const frameRate = Math.ceil(totalFrames / parseInt(duration));
const timestamp = Date.now();
const firstImageId = path.basename(sortedImages[0]).match(/\d+/)[0];
const lastImageId = path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0];
const outputVideo = path.join(
workdir,
`${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`
);
// le fichier final prend cette forme : {projectId}_{firstImageId}_{lastImageId}-{timestamp}.mp4 // Mise à jour initiale de la base de données
const timestamp = new Date().getTime(); let edit_video = {
const outputVideo = path.join(workdir, `${projectId}_${firstImageId}_${lastImageId}-${timestamp}.mp4`); status: 3,
progress: 0,
started_at: new Date(),
updated_at: new Date(),
eta: null
}
await database_manager.video.edit_video_by_id(videoId, edit_video)
const scale = res_width && res_height ? `scale=${res_width}:${res_height}` : 'scale=854:480'; // Redimensionne la vidéo en 480p par défaut
// Configuration de FFmpeg
const ffmpegArgs = [
'-y',
'-r', frameRate.toString(),
'-f', 'concat',
'-safe', '0',
'-i', tempFile,
'-vsync', 'vfr',
'-pix_fmt', 'yuv420p',
'-vf', scale,
'-b:v', '1500k', // Force un bitrate vidéo de 1500 kbps (ajuste si nécessaire)
outputVideo
];
ffmpegProcess = spawn('ffmpeg', ffmpegArgs, {
stdio: ['ignore', 'ignore', 'pipe']
});
let lastUpdate = 0;
const startTime = Date.now();
// Écoute de la sortie d'erreur pour la progression
ffmpegProcess.stderr.on('data', (data) => {
const output = data.toString();
const frameMatch = output.match(/frame=\s*(\d+)/);
if (frameMatch) {
const currentFrame = parseInt(frameMatch[1], 10);
const progress = Math.min((currentFrame / totalFrames) * 100, 99.99);
const now = Date.now();
// Calcul de l'ETA
const elapsedSeconds = (now - startTime) / 1000;
const eta = elapsedSeconds / (currentFrame / totalFrames) - elapsedSeconds;
// Mise à jour max toutes les 500ms
if (now - lastUpdate > 500) {
let update_video = {
progress: progress,
eta: Math.round(eta),
updated_at: new Date()
}
database_manager.video.edit_video_by_id(videoId, update_video)
console.log('Progress:', progress.toFixed(2), '%, ETA:', eta.toFixed(0), 's');
lastUpdate = now;
}
}
});
// Attente de la fin du processus
await new Promise((resolve, reject) => {
ffmpegProcess.on('close', async (code) => {
if (code === 0) {
try {
// Mise à jour finale
// await db.query(`
// UPDATE public.videos
// SET
// status = 1,
// progress = 100,
// eta = 0,
// video_file = $1,
// updated_at = NOW()
// WHERE id = $2
// `, [outputVideo, videoId]);
let latest_update = {
status: 1,
progress: 100,
eta: 0,
video_file: outputVideo,
updated_at: new Date()
}
await database_manager.video.edit_video_by_id(videoId, latest_update)
resolve();
} catch (e) {
reject(e);
}
} else {
reject(new Error(`FFmpeg process exited with code ${code}`));
}
});
ffmpegProcess.on('error', reject);
});
// Commande ffmpeg pour créer la vidéo
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
console.log('Running ffmpeg command:', ffmpegCommand);
execSync(ffmpegCommand);
console.log('Video created successfully:', outputVideo);
return outputVideo; return outputVideo;
} catch (error) {
console.error('Error creating video:', error);
serverError(error);
} finally {
// Supprimer le fichier temporaire
if (fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile);
console.log('Temporary file deleted:', tempFile);
}
}
}
async function createVideo(projectId) { } catch (error) {
const tempFile = path.join('temp.txt'); // Gestion des erreurs
console.error('Error in video creation:', error);
try { try {
// Trouver tous les fichiers image pour le projet donné // Mise à jour de la base de données en cas d'erreur
const workdir = path.join(PROJECTS_DIR, 'storage', `${projectId}`); let error_video = {
const dir = path.join(PROJECTS_DIR, 'storage', `${projectId}`, 'images'); status: 0,
console.log('dir:', dir); progress: 0,
const images = storageManager.scanAllImages(dir); eta: null,
console.log('images:', images); updated_at: new Date()
}
await database_manager.video.edit_video_by_id(videoId, error_video)
} catch (dbError) {
console.error('Database update error:', dbError);
}
// Trier les images numériquement throw error;
const sortedImages = images.sort((a, b) => {
const numA = parseInt(path.basename(a).match(/\d+/)[0], 10);
const numB = parseInt(path.basename(b).match(/\d+/)[0], 10);
return numA - numB;
});
// En déduire l'id de la première et dernière image utilisée
const firstImageId = parseInt(path.basename(sortedImages[0]).match(/\d+/)[0], 10);
const lastImageId = parseInt(path.basename(sortedImages[sortedImages.length - 1]).match(/\d+/)[0], 10);
console.log('firstImageId:', firstImageId);
console.log('lastImageId:', lastImageId);
// Créer un fichier temporaire pour la liste des images
fs.writeFileSync(tempFile, sortedImages.map(image => `file '${image}'`).join('\n'));
const frameRate = 10;
const outputVideo = path.join(workdir, 'video.mp4');
// Commande ffmpeg pour créer la vidéo
const ffmpegCommand = `ffmpeg -r ${frameRate} -f concat -safe 0 -i ${tempFile} -vsync vfr -pix_fmt yuv420p ${outputVideo}`;
console.log('Running ffmpeg command:', ffmpegCommand);
execSync(ffmpegCommand);
console.log('Video created successfully:', outputVideo);
} catch (error) {
console.error('Error creating video:', error);
serverError(error);
} finally { } finally {
// Supprimer le fichier temporaire // Nettoyage
if (fs.existsSync(tempFile)) { if (!cleanupDone) {
if (tempFile && fs.existsSync(tempFile)) {
fs.unlinkSync(tempFile); fs.unlinkSync(tempFile);
console.log('Temporary file deleted:', tempFile); }
if (ffmpegProcess) {
ffmpegProcess.kill();
}
cleanupDone = true;
} }
} }
} }
module.exports = { createVideo, createVideoWithList }; module.exports = { createVideoWithList };

View File

@@ -27,3 +27,4 @@ Routes :
- /data/image/:id = image depuis le pool de stockage - /data/image/:id = image depuis le pool de stockage
- /data/video/:id = vidéo depuis le pool de stockage - /data/video/:id = vidéo depuis le pool de stockage
-

View File

@@ -1,51 +1,14 @@
const storageManager = require('../src/data/storageManager'); import path from 'path';
const videoManager = require('../src/video/videoManager'); import { fileURLToPath } from 'url';
const measureManager = require('../src/measure/measureManager');
const path = require('path');
// console.log('Testing database functions...'); const __dirname = path.dirname(fileURLToPath(import.meta.url));
try {
storageManager.createFolder('test_folder');
console.log('1 - Folder created');
storageManager.deleteFolder('test_folder');
console.log('2 - Folder deleted');
} catch (error) {
console.error('Error testing database functions:', error);
}
function getSmileImage() { function getSmileImage() {
return path.join(__dirname, '../sample/smile.png'); return path.join(__dirname, '../sample/smile.png');
} }
//test de lancement d'une création de vidéo sur le projet 1 function getCatVideo() {
// videoManager.createVideo(1).then(res => { return path.join(__dirname, '../sample/cat.mp4');
// console.log('3 - Video created:', res); }
// }).catch(err => {
// console.error('Error creating video:', err);
// });
// async function run() {
// var Path = await measureManager.getPathFromIds(1, 1);
// console.log(Path);
// }
// run().catch(err => { export { getSmileImage, getCatVideo };
// console.error('Error:', err);
// });
var pathList = [
'storage/1/images/1.jpg',
'storage/1/images/10.jpg',
'storage/1/images/20.jpg',
'storage/1/images/30.jpg',
];
videoManager.createVideoWithList(1, pathList).then(res => {
console.log('3 - Video created:', res);
return storageManager.deleteFile(res);
}).then(res => {
console.log('4 - Video deleted:', res);
}).catch(err => {
console.error('Error:', err);
});
exports.getSmileImage = getSmileImage;

View File

@@ -1,6 +1,12 @@
function sendError(comment, res, err) { function sendError(comment, res = { status: () => ({ json: () => {} }) }, err = null, code = 500) {
console.error(comment, err); console.error(comment, err);
res.status(500).send('Server error'); res.status(code).json({
error: {
message: comment,
code: code,
error: err
}
});
} }
module.exports = { module.exports = {