export video/image

This commit is contained in:
2026-05-15 16:14:26 +02:00
parent 4fb7fa8fd3
commit 7760d7ae7c
13 changed files with 183 additions and 149 deletions
+15 -39
View File
@@ -7,7 +7,7 @@ import posix_ipc
import mmap
import cv2
import numpy as np
from django.http import JsonResponse, HttpResponse
#from django.http import JsonResponse, HttpResponse
from django.conf import settings
from reduct.time import unix_timestamp_to_iso
@@ -38,15 +38,10 @@ async def remove_video_by_uuid(uuid, start_ts=None, end_ts=None, when=None):
async def remove_video(uuid, start_ts, end_ts, when=None):
try:
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
return JsonResponse({'state': 'ok'}, status=200)
except Exception as e:
return JsonResponse({'error': str(e)}, status=500)
await remove_video_by_uuid(uuid, start_ts, end_ts, when=when)
async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
try:
try:
record_manager = CameraRecordManager(cameraDB)
total_size = await record_manager.size(uuid, start_ts, end_ts)
@@ -79,7 +74,8 @@ async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc
total += 1
if not frame_sizes:
return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)
raise Exception("No frame found!")
#return JsonResponse({'error': 'Aucune frame trouvée'}, status=404)
video_path = os.path.join(settings.MEDIA_ROOT, f"output.{opencv_video_type}")
fourcc = cv2.VideoWriter_fourcc(* opencv_fourcc_format)
@@ -97,36 +93,16 @@ async def shm_download_video(uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc
video.write(frame)
current_offset += size
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
i+=1
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
i+=1
video.release()
# Nettoie la mémoire partagée
shm.unlink()
# Vérifier que le fichier existe
if not os.path.exists(video_path):
logger.error(f"Fichier non créé: {video_path}")
return JsonResponse({'error': 'Erreur création vidéo'}, status=500)
# Lit le fichier vidéo généré
with open(video_path, 'rb') as f:
video_bytes = f.read()
# Retourne la vidéo en réponse
response = HttpResponse(video_bytes, content_type='video/mp4')
response['Content-Disposition'] = f'attachment; filename="{video_path}"'
response['Content-Length'] = os.path.getsize(video_path)
# Supprime le fichier temporaire
os.remove(video_path)
return response
return { "status": 404, "success": True, "video_path": video_path}
except Exception as e:
logger.error(f"shm_download_video: {e}")
return JsonResponse({'error': str(e)}, status=500)
return {"status": 500, "success": False, "video_path": video_path, "error": str(e)}
# ─────────────────────────────────────────────
##
@@ -247,7 +223,7 @@ async def export_images_zip(
total += 1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
return {"status": "error", "message": "No frame found!..."}
# --- Génération du ZIP ---
max_zip_bytes = max_zip_size_mb * 1024 * 1024 if max_zip_size_mb > 0 else 0
@@ -297,7 +273,7 @@ async def export_images_zip(
zf.writestr(f"{uuid}_{ts_iso}.jpg", buf.tobytes())
written += 1
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
i+=1
current_offset += size
@@ -402,7 +378,7 @@ async def export_video_mp4(
total +=1
if not frame_sizes:
return {"status": "error", "message": "Aucune frame trouvée"}
return {"status": "error", "message": "No frame found!..."}
# --- Génération du MP4 ---
@@ -458,14 +434,14 @@ async def export_video_mp4(
written += 1
current_offset += size
progress_bar(i + 1, total, prefix=f'Progression {uuid}:', suffix='Terminé', length=30)
progress_bar(i + 1, total, prefix=f'Progress {uuid}:', suffix='Ended', length=30)
i+=1
if video:
video.release()
if not os.path.exists(video_path):
return {"status": "error", "message": f"Fichier {opencv_video_type} non créé"}
return {"status": "error", "message": f"File {opencv_video_type} not created!..."}
## Copie vers les destinations (local + Samba)
filename = os.path.basename(video_path)
+14 -14
View File
@@ -42,7 +42,7 @@ TUBE_AXIS_TYPE = [
class Configuration(models.Model):
name = models.CharField(_("Nom de la Configuration"), help_text=_("Nom de la configuration"), max_length=100, null=True, blank=False, default=_("Configuration par défaut"))
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
# Dashboard configuration
sidebar_width = models.CharField(_("Barre latérale"), help_text=_("Largeur barre latérale (css)"), max_length=32, null=True, blank=False, default="350px")
default_grid_columns = models.PositiveSmallIntegerField(_("Colonnes de la grille par défaut"), help_text=_("Nombre de colonnes de la grille par défaut"), blank=False, default=3)
@@ -94,7 +94,7 @@ class Configuration(models.Model):
return f'{self.name}'
class Well(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
name = models.CharField(_("Nom"), help_text=_("Nom du puit: Ai..Di"), unique=True, max_length=4, null=True, blank=True)
class Meta:
@@ -110,7 +110,7 @@ class Well(models.Model):
class MultiWell(models.Model):
# Identification
label = models.CharField(_("Label"), help_text=_("Label du multi-puit"), max_length=100, null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
position = models.CharField(_("Position"), help_text=_('Position du multi-puits sur la table'), unique=True, max_length=8, choices=MULTIWELL_POSITION, null=True, blank=False)
default = models.BooleanField(_("Par défaut"), help_text=_('Multi-puit par défaut'), default=False)
# Configuration
@@ -175,7 +175,7 @@ class MultiWell(models.Model):
class WellPosition(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True)
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
@@ -239,7 +239,7 @@ class Experiment(models.Model):
comment = models.TextField(_("Commentaires"), help_text=_("Descriptions de l'expérience"), null=True, blank=True)
identifier = models.CharField(_("Identifiant d'expérience"), unique=True, max_length=100, null=True, blank=False )
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
multiwell = models.ForeignKey(MultiWell, verbose_name=_("Multi-puits"), on_delete=models.SET_NULL, null=True, blank=True)
duration = models.PositiveIntegerField(_("Durée"), help_text=_('Durée de la prise de vue en secondes'), blank=False, default=120)
@@ -249,7 +249,7 @@ class Experiment(models.Model):
def save(self, *args, **kwargs):
self.identifier = f'{self.id}-{self.multiwell.position}_{self.created.isoformat()[:19]}'
self.identifier = f'{self.multiwell.position}_{self.created.isoformat()[:19]}'
super().save(*args, **kwargs)
@@ -264,7 +264,7 @@ class Experiment(models.Model):
verbose_name_plural = _("Expériences")
def __str__(self):
return f'{self.identifier}'
return f'{self.identifier} [ {self.title} ]'
class Session(models.Model):
@@ -276,7 +276,7 @@ class Session(models.Model):
ERROR = "error", _("Erreur")
name = models.CharField(_("Nom de la session"), help_text=_("Session d'expérience. 4 Multi-puits maximum"), max_length=100, null=True, blank=False)
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
active = models.BooleanField(_("Active"), default=True)
expected_export = models.DateTimeField(_("Date d'exportation"), help_text=_("Date d'exportation prévue"), null=True, blank=True)
expected_scanning = models.DateTimeField(_("Date du balayage"), help_text=_("Date du balayage prévue"), null=True, blank=True)
@@ -313,7 +313,7 @@ class Session(models.Model):
def __str__(self):
state = _("Terminée") if not self.active else _("Active")
return f'{self.id}: {self.name} {state}'
return f'[ {self.id} ] {self.name} ({state})'
@receiver(post_save, sender=Session)
@@ -376,9 +376,9 @@ def delete_periodic_task(sender, instance, **kwargs):
class SessionExperiment(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.SET_NULL, null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="session_experiments")
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
session = models.ForeignKey(Session, verbose_name=_("Session"), on_delete=models.CASCADE, null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="session_experiments")
@classmethod
def experiment_by_session(cls, session_id, active=True):
@@ -407,8 +407,8 @@ class SessionExperiment(models.Model):
class ExperimentWell(models.Model):
author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.SET_NULL, null=True, blank=True, related_name="experimentwell")
author = models.ForeignKey(User, on_delete=models.SET_NULL, verbose_name="Auteur", null=True, blank=True)
experiment = models.ForeignKey(Experiment, verbose_name=_("Expérience"), on_delete=models.CASCADE, null=True, blank=True, related_name="experimentwell")
well = models.ForeignKey(Well, verbose_name=_("Puit"), on_delete=models.SET_NULL, null=True, blank=True, related_name="wellexperiment")
active = models.BooleanField(_("Active"), default=True)
+8 -4
View File
@@ -151,7 +151,7 @@ class MultiWellManager:
multiwell = experiment.multiwell
if self.process.use_tracking:
cfg = ExperimentConfig.objects.filter(experiment_id=experiment.id, well_id=well.id).first()
cfg = ExperimentConfig.objects.filter(experiment_key_id=experiment.id, well=well.name).first()
if not cfg:
raise Exception(f"Configuration d'expérience introuvable pour {experiment} / {well}")
# reset PlanarianTracker => on_well_change
@@ -159,14 +159,16 @@ class MultiWellManager:
## create uuid for this capture
uuid = f'{self.process.data.session}-{multiwell.position}-{well.name}'
## start recording
self.process.data.uuid = uuid
if not simulate:
self.process.data.record = True
self.process._send(current=well_position.order)
logger.info(f"Starting capture for {uuid} ordre: {well_position.order}")
msg = f"Starting capture for {uuid} ordre: {well_position.order}"
logger.info(msg)
self.process._send(well_state=msg)
start = time.monotonic()
while not self.stop_playing.is_set():
@@ -249,7 +251,9 @@ class MultiWellManager:
started = timezone.now()
for obs in experiments:
logger.warning(f"Starting scan for {obs} (well {pos}/{len(experiments)})")
msg = f"Starting scan for {obs} (well {pos}/{len(experiments)})"
logger.warning(msg)
self.process._send(well_state=msg)
if self.stop_playing.is_set():
break
@@ -25,6 +25,7 @@ class ScannerManager {
this.scan_state = options.scan_state;
this.sim_bt = options.simulate || null;
this.well_btn = options.well_btn;
this.well_state = options.well_state;
}
init_controls() {
@@ -56,7 +57,8 @@ class ScannerManager {
}
if (payload.ts) { this.ts.textContent = timestampToLocalISOString(payload.ts); }
if (payload.scan_state) { this.scan_state.textContent=payload.scan_state;}
if (payload.well_state) { this.well_state.textContent=payload.well_state;}
if (payload.buttons) {
this.well_btn.innerHTML = payload.buttons;
const span = this.crop.querySelector("span.crop"); span.style.color = '#f0f';
@@ -66,9 +68,7 @@ class ScannerManager {
if (btn.value==payload.current) { btn.classList.add('w3-green'); return; }
btn.classList.remove('w3-green');
});
}
}
} catch(e) { console.log(e); }
}
+29 -15
View File
@@ -5,7 +5,7 @@ import requests
from celery import shared_task, group, chord, chain
from celery.utils.log import get_task_logger
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from .process import ScannerProcess, ReplayProcess, redisDB
from .export_tasks import shm_download_video, export_images_zip, export_video_mp4
from .constants import ScannerConstants
@@ -18,21 +18,35 @@ logger = get_task_logger(__name__)
def scanner_start():
scanner = ScannerProcess()
scanner.start()
return f"Scanner démarré."
return str(_("Scanner démarré."))
@shared_task
def replay_start():
replay = ReplayProcess(latency=5.0)
replay.start()
return f"Replay démarré."
return str(_("Replay démarré."))
@shared_task
def download_video(uuid, start_ts, end_ts, frame_rate=10, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
try:
return asyncio.run(shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type))
@shared_task(bind=True)
def download_video(self, uuid, start_ts, end_ts, frame_rate=5, opencv_fourcc_format='mp4v', opencv_video_type='mp4'):
"""
Exécuter directement la fonction async DANS la tâche Celery.
Sans passer par une wrapper.
"""
try:
logger.info(f"Task {self.request.id}: Processing {uuid} from {start_ts} to {end_ts}")
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
shm_download_video(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type) )
return result
finally:
loop.close()
except Exception as e:
logger.error(f"download_video: {e}")
logger.error(f"shm_download_video_task error: {e}", exc_info=True)
return { 'status': 'error', 'error': str(e), 'uuid': uuid}
@shared_task
def export_images(
uuid: str,
@@ -46,7 +60,8 @@ def export_images(
return asyncio.run(export_images_zip(uuid, start_ts, end_ts, jpeg_quality, max_zip_size_mb, max_image_width, max_image_height))
except Exception as e:
logger.error(f"export_images: {e}")
@shared_task
def export_all_images(session_id=None):
try:
@@ -72,13 +87,11 @@ def export_all_images(session_id=None):
return job_zip
except Exception as e:
logger.error(f"export_images: {e}")
logger.error(f"export_images: {e}")
@shared_task
def export_videos(uuid: str,
start_ts: float | None = None,
end_ts: float | None = None,
frame_rate: int = 5,
def export_videos(uuid: str, start_ts: float | None = None, end_ts: float | None = None, frame_rate: int = 5,
opencv_fourcc_format='mp4v',
opencv_video_type='mp4',
max_video_size_mb: int = 0,
@@ -88,6 +101,7 @@ def export_videos(uuid: str,
return asyncio.run(export_video_mp4(uuid, start_ts, end_ts, frame_rate, opencv_fourcc_format, opencv_video_type, max_video_size_mb, max_width, max_height))
except Exception as e:
logger.error(f"export_videos: {e}")
return {"success": False, "error": str(e)}
@shared_task
@@ -0,0 +1,6 @@
{% for ss in experiments %}
<div>
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
<label>{{ ss }}</label>
</div>
{% endfor %}
@@ -16,18 +16,15 @@
{% endfor %}
</select>
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
<label>{{ ss.multiwell }}</label>
{% endfor %}
{% include "scanner/experiment-inc.html" %}
</div>
{% if current_session.id and current_experiment %}
{% multiwell_cards current_session.id current_experiment %}
{% endif %}
</form>
{% if current_session.id %}
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images()">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images" %}<br>
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_images({{ current_session.id }})">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des images de la session" %}<br>
<span class="w3-margin-left">{{ export_destination }}
</a>
{% endif %}
@@ -65,7 +62,8 @@
const duration = parseInt("{{ duration }}");
const uuid_btn = sId("btn-" + "{{ uuid }}");
const image_endpoint = "{% url 'scanner:export_api' %}";
const sid = parseInt("{{ cursid }}");
const export_msg = "{% trans "Confirmer l'export des images." %}";
const export_msg_long = "{% trans "ATTENTION le processus peut être long!" %}";
function setGridColumns(col) { container.style.setProperty("--grid-columns", col); updateActiveButton(col); }
function handleMultiwell(b) { b.classList.add('w3-green'); }
@@ -78,7 +76,7 @@
}
function image_save(name, img) {
const ok = confirm(`Télécharger le fichier ?\n\n${name}`);
const ok = confirm(export_msg);
if (!ok) return false;
fetch(img)
.then(res => res.blob())
@@ -94,7 +92,9 @@
});
}
function download_all_images() {
function download_all_images(sid) {
const ok = confirm(export_msg +' '+ export_msg_long);
if (!ok) return false;
fetch(image_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -109,7 +109,8 @@
})
.catch(error => {
console.error('Erreur:', error);
});
});
}
setGridColumns(columns);
if (uuid_btn)
@@ -17,10 +17,7 @@
{% endfor %}
</select>
<div class="w3-margin-left w3-margin-bottom">
{% for ss in experiments %}
<input class="" type="radio" name="_expid" value="{{ ss.id }}" {% if ss.id == current_experiment.id %}checked{% endif %} onchange="this.form.submit()" >
<label>{{ ss.multiwell }}</label>
{% endfor %}
{% include "scanner/experiment-inc.html" %}
</div>
{% if current_session.id and current_experiment %}
@@ -28,7 +25,7 @@
{% endif %}
</form>
{% if current_session.id %}
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_videos()">
<a href="#" class="w3-bar-item w3-btn w3-hover-opacity" onclick="download_all_videos({{ current_session.id }})">
<i class="fa-solid fa-file-export w3-text-red w3-xlarge"></i> {% trans "Exporter l'ensemble des vidéos" %}<br>
<span class="w3-margin-left">{{ export_destination }}</span>
</a>
@@ -78,9 +75,12 @@
const ws_route = "{{ ws_route }}";
const uuid_btn = sId("btn-" + "{{ uuid }}");
const image_endpoint = "{% url 'scanner:export_api' %}";
const sid = parseInt("{{ cursid }}");
const export_msg = "{% trans "Confirmer l'export des vidéos. ATTENTION le processus peut être long!" %}";
function download_all_videos() {
function download_all_videos(sid) {
const ok = confirm(export_msg);
if (!ok) return false;
fetch(image_endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -51,7 +51,8 @@
</div>
{% endif %}
<div class="w3-col">
<div id="_well_btn" class="w3-margin-top"></div>
<div id="_well_state" class="w3-margin-top w3-small w3-text-amber w3-justify"></div>
<div id="_well_btn"></div>
</div>
<div class="w3-col">
<button id="_halt" class="w3-button w3-red w3-round-large w3-margin-top w3-block">
@@ -63,8 +64,8 @@
<div class="w3-margin-top w3-padding-small w3-small">
<span id="_ts"></span>
</div>
<div class="w3-margin-top w3-small w3-center w3-justify">
<span id="_scan_state" class="w3-text-amber w3-padding"></span>
<div class="w3-margin-top w3-small">
<span id="_scan_state" class="w3-text-amber w3-padding w3-justify"></span>
</div>
</div>
</div>
@@ -100,7 +101,9 @@
frame_count : sId("_count"),
scan_state : sId("_scan_state"),
simulate : sId("_simulate"),
well_btn : sId("_well_btn")
well_btn : sId("_well_btn"),
well_state : sId("_well_state")
};
</script>
<script src="/static/scanner/js/scanning.js"></script>
+33 -17
View File
@@ -1,13 +1,15 @@
#
import os
from asgiref.sync import async_to_sync
import base64, json
from django.shortcuts import render #, redirect
from django.http import JsonResponse
from django.http import JsonResponse, FileResponse
from django.utils.translation import gettext_lazy as _
from django.views.decorators.http import require_GET, require_POST
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth.decorators import login_required, user_passes_test
from django.conf import settings
from reduct.time import unix_timestamp_to_iso
from modules.system_stats import get_cached_stats, start_background_updater
from modules import reductstore
@@ -198,13 +200,28 @@ def images_view(request):
@csrf_exempt
def download_api(request):
data = json.loads(request.body.decode() or "{}")
action = data.get("action")
if action=='download':
uuid, dt_start, dt_stop, frame_rate = data.get("uuid"), data.get("dt_start"), data.get("dt_stop"), data.get("fps")
return download_video.delay(uuid, dt_start, dt_stop, frame_rate=frame_rate) # @UndefinedVariable
else:
return JsonResponse({"state": False})
try:
action = data.get("action")
if action=='download':
uuid, dt_start, dt_stop, frame_rate = data.get("uuid"), data.get("dt_start"), data.get("dt_stop"), data.get("fps")
task = download_video.delay(uuid, dt_start, dt_stop, frame_rate=frame_rate) # @UndefinedVariable
result = task.get(timeout=300)
if result.get('success'):
video_path = result.get('video_path')
if not os.path.exists(video_path):
raise Exception(f"File not created: {video_path}")
response = FileResponse(open(video_path, 'rb'), content_type='video/mp4')
response['Content-Disposition'] = f'attachment; filename="{video_path}"'
response['Content-Length'] = os.path.getsize(video_path)
os.remove(video_path)
return response
return JsonResponse(result)
except Exception as e:
print("download_api error", e)
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })
def get_video(uuid):
oldest, latest = async_to_sync(reductstore.old_last_dates)(cameraDB, entry_name=uuid)
@@ -246,14 +263,13 @@ def export_api(request):
data = json.loads(request.body.decode() or "{}")
session_id = data.get("sid")
action = data.get("action")
if action == 'export_images':
export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
elif action == 'export_videos':
export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
else:
return JsonResponse({"success": False, "msg": str(_("Erreur d'exportation"))})
try:
if action == 'export_images':
export_all_images(session_id)
return JsonResponse({"success": True, "msg": str(_("Images téléchargées"))})
elif action == 'export_videos':
export_all_videos(session_id)
return JsonResponse({"success": True, "msg": str(_("Vidéos téléchargées"))})
except:
return JsonResponse({"state": False, "success": False, "msg": str(_("Erreur d'exportation")), })