diff --git a/pod/import_video/admin.py b/pod/import_video/admin.py
index 4791ac2528..319a9cdb1b 100644
--- a/pod/import_video/admin.py
+++ b/pod/import_video/admin.py
@@ -17,6 +17,7 @@ class ExternalRecordingAdmin(admin.ModelAdmin):
"start_at",
"type",
"source_url",
+ "state",
"owner",
)
search_fields = [
diff --git a/pod/import_video/forms.py b/pod/import_video/forms.py
index e14f8eb61c..9bbbcc9789 100644
--- a/pod/import_video/forms.py
+++ b/pod/import_video/forms.py
@@ -77,8 +77,8 @@ def __init__(self, *args, **kwargs):
self.filter_fields_admin()
self.fields = add_placeholder_and_asterisk(self.fields)
- # We don't change the user who uploaded the record
- hidden_fields = ("uploaded_to_pod_by",)
+ # We don't change the user who uploaded the record neither the state
+ hidden_fields = ("uploaded_to_pod_by", "state",)
for field in hidden_fields:
if self.fields.get(field, None):
self.fields[field].widget = forms.HiddenInput()
diff --git a/pod/import_video/models.py b/pod/import_video/models.py
index ee804661a1..a724b77a0a 100644
--- a/pod/import_video/models.py
+++ b/pod/import_video/models.py
@@ -102,6 +102,13 @@ class ExternalRecording(models.Model):
),
)
+ state = models.CharField(
+ max_length=250,
+ verbose_name=_("Recording state"),
+ blank=True,
+ null=True,
+ )
+
# User who uploaded to Pod the video file
uploaded_to_pod_by = models.ForeignKey(
User,
diff --git a/pod/import_video/templates/import_video/list.html b/pod/import_video/templates/import_video/list.html
index f0e9513694..2906f44c75 100644
--- a/pod/import_video/templates/import_video/list.html
+++ b/pod/import_video/templates/import_video/list.html
@@ -52,7 +52,7 @@
{% for record in recordings %}
{{ record.name }} |
- {{ record.state }} |
+ {{ record.state | safe }} |
{{ record.startTime }} |
{{ record.type }} |
@@ -61,12 +61,13 @@
{% endif %}
- {% if record.videoUrl != "" %}
+ {% if record.videoUrl != "" and record.videoUrl != record.presentationUrl %}
{% endif %}
- |
+ |
+
{% if record.canUpload %}
{% trans "Please confirm you want to upload the recording to Pod" as confirmUpload %}
{% endif %}
- |
+ |
+
{% if record.canDelete %}
{% trans "Please confirm you want to delete the external recording" as confirmDelete %}
diff --git a/pod/import_video/tests/test_views.py b/pod/import_video/tests/test_views.py
index 3d1321069b..93be0a1663 100644
--- a/pod/import_video/tests/test_views.py
+++ b/pod/import_video/tests/test_views.py
@@ -5,7 +5,10 @@
"""
+import os
from ..models import ExternalRecording
+from ..views import bbb_encode_presentation, bbb_encode_presentation_and_upload_to_pod
+from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.test import TestCase
@@ -14,6 +17,13 @@
from django.utils.translation import ugettext_lazy as _
from http import HTTPStatus
+# Directory that will contain the video files generated by bbb-recorder
+IMPORT_VIDEO_BBB_RECORDER_PATH = getattr(
+ settings,
+ "IMPORT_VIDEO_BBB_RECORDER_PATH",
+ "/data/bbb-recorder/media/"
+)
+
class ExternalRecordingDeleteTestView(TestCase):
"""List of view tests for deleting an external recording.
@@ -318,3 +328,34 @@ def test_recording_upload_post_request(self):
self.assertTrue(unable_str in response.content)
print(" ---> test_recording_upload_get_request of RecordingUploadTestView: OK!")
+
+ def test_recording_encode_presentation(self):
+ """Test recording encode presentation."""
+ self.user = User.objects.get(username="pod")
+ self.client.force_login(self.user)
+
+ # Encode presentation
+ recordingBBB = ExternalRecording.objects.get(name="test external bbb recording1")
+ self.user = User.objects.get(username="pod2")
+ self.client.force_login(self.user)
+ expected = os.path.join(IMPORT_VIDEO_BBB_RECORDER_PATH, "video.webm")
+ actual = bbb_encode_presentation(
+ "video.webm",
+ recordingBBB.source_url,
+ )
+ self.assertEqual(actual, expected)
+ # Encode presentation and move to destination
+ # Expected state
+ expected = _(
+ "Impossible to upload to Pod the video, "
+ "the link provided does not seem valid."
+ )
+ bbb_encode_presentation_and_upload_to_pod(
+ recordingBBB.id,
+ recordingBBB.source_url,
+ "webm"
+ )
+ recordingBBB = ExternalRecording.objects.get(name="test external bbb recording1")
+ self.assertEqual(recordingBBB.state, expected)
+
+ print(" ---> test_recording_encode_presentation of RecordingUploadTestView: OK!")
diff --git a/pod/import_video/utils.py b/pod/import_video/utils.py
index 1eac01ffb1..37a7561a23 100644
--- a/pod/import_video/utils.py
+++ b/pod/import_video/utils.py
@@ -1,4 +1,4 @@
-"""Utils for Meeting and Import_video module."""
+"""Esup-Pod meeting and import_video utils."""
import json
import os
@@ -7,12 +7,14 @@
from datetime import datetime as dt
from django.conf import settings
+from django.contrib.auth.models import User
from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _
from html.parser import HTMLParser
from pod.video.models import Video
from pod.video.models import Type
from urllib.parse import parse_qs, urlparse
+from requests import Session
MAX_UPLOAD_SIZE_ON_IMPORT = getattr(settings, "MAX_UPLOAD_SIZE_ON_IMPORT", 4)
@@ -67,7 +69,7 @@ def secure_request_for_upload(request):
raise ValueError(msg)
-def parse_remote_file(session, source_html_url):
+def parse_remote_file(session: Session, source_html_url: str):
"""Parse the remote HTML file on the BBB server.
Args:
@@ -140,7 +142,7 @@ def create_parser(response):
return parser
-def manage_recording_url(source_url, video_file_add):
+def manage_recording_url(source_url: str, video_file_add: str) -> str:
"""Generate the BBB video URL.
See more explanations in manage_download() function.
@@ -181,7 +183,12 @@ def manage_recording_url(source_url, video_file_add):
return source_url + video_file_add
-def manage_download(session, source_url, video_file_add, dest_file):
+def manage_download(
+ session: Session,
+ source_url: str,
+ video_file_add: str,
+ dest_file: str
+) -> str:
"""Manage the download of a BBB video file.
2 possibilities :
@@ -195,7 +202,7 @@ def manage_download(session, source_url, video_file_add, dest_file):
and download_video_file.
Args:
- session (Session) : session useful to achieve requests (and keep cookies between)
+ session (Session) : Session useful to achieve requests (and keep cookies between)
source_url (String): Source file URL
video_file_add (String): Name of the video file to add to the URL
dest_file (String): Destination file of the Pod video
@@ -214,11 +221,11 @@ def manage_download(session, source_url, video_file_add, dest_file):
raise ValueError(mark_safe(str(exc)))
-def download_video_file(session, source_video_url, dest_file):
+def download_video_file(session: Session, source_video_url: str, dest_file: str):
"""Download video file.
Args:
- session (Session) : session useful to achieve requests (and keep cookies between)
+ session (Session) : Session useful to achieve requests (and keep cookies between)
source_video_url (String): Video file URL
dest_file (String): Destination file of the Pod video
@@ -250,15 +257,21 @@ def download_video_file(session, source_video_url, dest_file):
raise ValueError(mark_safe(str(exc)))
-def save_video(request, dest_path, recording_name, description, date_evt=None):
+def save_video(
+ user: User,
+ dest_path: str,
+ recording_name: str,
+ description: str,
+ date_evt=None
+):
"""Save and encode the Pod video file.
Args:
- request (Request): HTTP request
+ user (User): User who saved the video
dest_path (String): Destination path of the Pod video
- recording_name (String): recording name
- description (String): description added to the Pod video
- date_evt (Datetime, optional): Event date. Defaults to None.
+ recording_name (String): Recording name
+ description (String): Description added to the Pod video
+ date_evt (Datetime, optional): Event date. Default to None
Raises:
ValueError: if impossible creation
@@ -267,7 +280,7 @@ def save_video(request, dest_path, recording_name, description, date_evt=None):
video = Video.objects.create(
video=dest_path,
title=recording_name,
- owner=request.user,
+ owner=user,
description=description,
is_draft=True,
type=Type.objects.get(id=DEFAULT_TYPE_ID),
@@ -283,14 +296,14 @@ def save_video(request, dest_path, recording_name, description, date_evt=None):
raise ValueError(msg)
-def check_file_exists(source_url):
- """Check that the URL exists.
+def check_url_exists(source_url: str) -> bool:
+ """Check that the source URL exists.
Args:
source_url (String): Source URL
Returns:
- Boolean: file exists (True) or not (False)
+ Boolean: URL exists (True) or not (False)
"""
try:
response = requests.head(source_url, timeout=2)
@@ -302,7 +315,7 @@ def check_file_exists(source_url):
return False
-def verify_video_exists_and_size(video_url):
+def verify_video_exists_and_size(video_url: str):
"""Check that the video file exists and its size does not exceed the limit.
Args:
@@ -329,7 +342,7 @@ def verify_video_exists_and_size(video_url):
raise ValueError(msg)
-def check_video_size(video_size):
+def check_video_size(video_size: int):
"""Check that the video file size does not exceed the limit.
Args:
@@ -349,12 +362,13 @@ def check_video_size(video_size):
raise ValueError(msg)
-def check_source_url(source_url): # noqa: C901
+def check_source_url(source_url: str): # noqa: C901
"""Check the source URL to identify the used platform.
Platforms managed :
- Mediacad platform (Médiathèque académique) : rewrite source URL if required
and manage JSON API.
+ - Old BigBlueButton : source URL for old BBB presentation playback
"""
base_url = ""
media_id = ""
@@ -421,6 +435,22 @@ def check_source_url(source_url): # noqa: C901
media_id,
)
platform = "Mediacad"
+ elif source_url.find("/playback/presentation/2.0/playback.html?") != -1:
+ # Old BBB 2.x (<2.3) presentation link
+ # Conversion from
+ # https://xxx/playback/presentation/2.0/playback.html?meetingId=ID
+ # to https://xxx/playback/presentation/2.3/ID?meetingId=ID
+ media_id = array_url[-1]
+ source_video_url = source_url.replace(
+ "/2.0/playback.html", "/2.3/" + media_id
+ ).replace("playback.html?meetingId=", "")
+ format = "webm"
+ platform = "BBB_Presentation"
+ elif source_url.find("/playback/presentation/2.3/") != -1:
+ # Old BBB 2.3 presentation link : no conversion needed
+ source_video_url = source_url
+ format = "webm"
+ platform = "BBB_Presentation"
# Platform's URL identified
if platform == "Mediacad":
@@ -431,6 +461,11 @@ def check_source_url(source_url): # noqa: C901
platform_type = TypeSourceURL(
platform, source_video_url, format, url_api_video
)
+ if platform == "BBB_Presentation":
+ # Platform type: older BBB, format presentation
+ platform_type = TypeSourceURL(
+ platform, source_video_url, format, ""
+ )
return platform_type
except Exception as exc:
@@ -440,30 +475,45 @@ def check_source_url(source_url): # noqa: C901
raise ValueError(msg)
-def define_dest_file(request, id, extension):
- """Define standard destination filename for an external recording."""
+def define_dest_file_and_path(user: User, id: str, extension: str):
+ """Define standard destination filename and path for an external recording."""
# Set a discriminant
discrim = dt.now().strftime("%Y%m%d%H%M%S")
dest_file = os.path.join(
settings.MEDIA_ROOT,
VIDEOS_DIR,
- request.user.owner.hashkey,
+ user.owner.hashkey,
os.path.basename("%s-%s.%s" % (discrim, id, extension)),
)
- os.makedirs(os.path.dirname(dest_file), exist_ok=True)
- return dest_file
-
-
-def define_dest_path(request, id, extension):
- """Define standard destination path for an external recording."""
- # Set a discriminant
- discrim = dt.now().strftime("%Y%m%d%H%M%S")
dest_path = os.path.join(
VIDEOS_DIR,
- request.user.owner.hashkey,
+ user.owner.hashkey,
os.path.basename("%s-%s.%s" % (discrim, id, extension)),
)
- return dest_path
+ os.makedirs(os.path.dirname(dest_file), exist_ok=True)
+ return dest_file, dest_path
+
+
+def check_file_exists(source: str) -> bool:
+ """Check that a local file exists."""
+ if os.path.exists(source):
+ return True
+ else:
+ return False
+
+
+def move_file(source: str, destination: str):
+ """Move a file from a source to another destination."""
+ try:
+ # Ensure that the source file exists
+ if not check_file_exists(source):
+ print(f"The source file '{source}' does not exist.")
+ return
+
+ # Move the file to the destination directory
+ shutil.move(source, destination)
+ except Exception as e:
+ print(f"Error moving the file: {e}")
class TypeSourceURL:
diff --git a/pod/import_video/views.py b/pod/import_video/views.py
index 9303c17511..5456cf9c5e 100644
--- a/pod/import_video/views.py
+++ b/pod/import_video/views.py
@@ -1,22 +1,29 @@
-"""Views of the Import_video module."""
-
+"""Esup-Pod import_video views.
+More information on this module at: https://www.esup-portail.org/wiki/x/BQCnSw
+"""
+import logging
import os
import requests
+import subprocess
+import threading
+import time
# For PeerTube download
import json
from .models import ExternalRecording
from .forms import ExternalRecordingForm
-from .utils import StatelessRecording, check_file_exists, download_video_file
+from .utils import StatelessRecording, check_url_exists, download_video_file
from .utils import manage_recording_url, check_source_url, parse_remote_file
from .utils import save_video, secure_request_for_upload
from .utils import check_video_size, verify_video_exists_and_size
-from .utils import define_dest_file, define_dest_path
+from .utils import define_dest_file_and_path, check_file_exists, move_file
+from .utils import TypeSourceURL
from datetime import datetime
from django.conf import settings
-from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.contrib.sites.shortcuts import get_current_site
from django.core.exceptions import PermissionDenied
from django.contrib import messages
from django.shortcuts import render, redirect
@@ -35,6 +42,10 @@
from pytube import YouTube
from pytube.exceptions import PytubeError, VideoUnavailable
+# To convert old BBB presentation
+from pod.main.tasks import task_start_bbb_presentation_encode_and_upload_to_pod
+from pod.main.tasks import task_start_bbb_presentation_encode_and_move_to_destination
+
RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY = getattr(
settings, "RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY", True
@@ -70,8 +81,31 @@
VIDEOS_DIR = getattr(settings, "VIDEOS_DIR", "videos")
+# Use of Celery to encode
+CELERY_TO_ENCODE = getattr(settings, "CELERY_TO_ENCODE", False)
+# Use plugin bbb-recorder for import-video module
+USE_IMPORT_VIDEO_BBB_RECORDER = getattr(
+ settings,
+ "USE_IMPORT_VIDEO_BBB_RECORDER",
+ False
+)
+# Directory of bbb-recorder plugin
+IMPORT_VIDEO_BBB_RECORDER_PLUGIN = getattr(
+ settings,
+ "IMPORT_VIDEO_BBB_RECORDER_PLUGIN",
+ "/data/bbb-recorder/"
+)
+# Directory that will contain the video files generated by bbb-recorder
+IMPORT_VIDEO_BBB_RECORDER_PATH = getattr(
+ settings,
+ "IMPORT_VIDEO_BBB_RECORDER_PATH",
+ "/data/bbb-recorder/media/"
+)
-def secure_external_recording(request, recording):
+log = logging.getLogger(__name__)
+
+
+def secure_external_recording(request, recording: ExternalRecording):
"""Secure an external recording.
Args:
@@ -95,7 +129,7 @@ def secure_external_recording(request, recording):
raise PermissionDenied
-def get_can_delete_external_recording(request, owner):
+def get_can_delete_external_recording(request, owner: User):
"""Return True if current user can delete this recording."""
can_delete = False
@@ -112,7 +146,7 @@ def get_can_delete_external_recording(request, owner):
@csrf_protect
@ensure_csrf_cookie
@login_required(redirect_field_name="referrer")
-def upload_external_recording_to_pod(request, record_id):
+def upload_external_recording_to_pod(request, record_id: int):
"""Upload external recording to Pod.
Args:
@@ -284,7 +318,7 @@ def add_or_edit_external_recording(request, id=None):
@csrf_protect
@ensure_csrf_cookie
@login_required(redirect_field_name="referrer")
-def delete_external_recording(request, id):
+def delete_external_recording(request, id: int):
"""Delete an external recording.
Args:
@@ -353,11 +387,11 @@ def save_recording_form(request, form):
# ############################## Upload recordings to Pod
-def save_external_recording(request, record_id):
+def save_external_recording(user: User, record_id: int):
"""Save an external recording in database.
Args:
- request (Request): HTTP request
+ user (User): user who saved this recording
record_id (Integer): id of the recording in database
Raises:
@@ -367,7 +401,7 @@ def save_external_recording(request, record_id):
# Update the external recording
recording, created = ExternalRecording.objects.update_or_create(
id=record_id,
- defaults={"uploaded_to_pod_by": request.user},
+ defaults={"uploaded_to_pod_by": user},
)
except Exception as exc:
msg = {}
@@ -376,7 +410,7 @@ def save_external_recording(request, record_id):
raise ValueError(msg)
-def upload_recording_to_pod(request, record_id):
+def upload_recording_to_pod(request, record_id: int) -> bool:
"""Upload recording to Pod (main function).
Args:
@@ -402,7 +436,7 @@ def upload_recording_to_pod(request, record_id):
elif recording.type == "peertube":
return upload_peertube_recording_to_pod(request, record_id)
else:
- # Upload a standard or BBB video file
+ # Upload a standard or BBB video file, or a BBB presentation
return upload_video_recording_to_pod(request, record_id)
except Exception as exc:
msg = {}
@@ -421,17 +455,30 @@ def upload_recording_to_pod(request, record_id):
raise ValueError(msg)
-def upload_video_recording_to_pod(request, record_id):
- """Upload a standard or BBB video file to Pod."""
+def upload_video_recording_to_pod(request, record_id: int): # noqa: C901
+ """Upload a standard or BBB video file, or a BBB presentation to Pod."""
try:
- # Try to identify the platform (avoids multiple source types)
+ # Try to identify the type of the source URL (avoids multiple source types)
type_source_url = check_source_url(request.POST.get("source_url"))
- # Mediacad platform
if type_source_url is not None and type_source_url.type == "Mediacad":
+ # Mediacad platform
return upload_mediacad_recording_to_pod(request, record_id, type_source_url)
+ elif type_source_url is not None and type_source_url.type == "BBB_Presentation":
+ # Old BigBlueBlutton playback (presentation format) :
+ # convert this presentation in video and upload automatically to Pod
+ # via an asynchronous task
+ if USE_IMPORT_VIDEO_BBB_RECORDER:
+ start_bbb_encode_presentation_and_upload_to_pod(
+ record_id,
+ type_source_url.url,
+ type_source_url.extension
+ )
+ return True
+ else:
+ return False
else:
# Video file (or BBB video file, same process) source URL
- return upload_bbb_recording_to_pod(request, record_id)
+ return upload_standard_video_recording_to_pod(record_id)
except Exception as exc:
msg = {}
proposition = ""
@@ -449,11 +496,11 @@ def upload_video_recording_to_pod(request, record_id):
raise ValueError(msg)
-def upload_bbb_recording_to_pod(request, record_id):
- """Upload a video file (or BBB video file) recording to Pod.
+def upload_standard_video_recording_to_pod(record_id: int) -> bool:
+ """Upload a standard video file (or BBB video file) recording to Pod.
+ Used with an URL.
Args:
- request (Request): HTTP request
record_id (Integer): id record in the database
Raises:
@@ -467,11 +514,12 @@ def upload_bbb_recording_to_pod(request, record_id):
session = requests.Session()
recording = ExternalRecording.objects.get(id=record_id)
- source_url = request.POST.get("source_url")
# Step 1: Download and parse the remote HTML file if necessary
# Check if extension is a video extension
+ source_url = recording.source_url
extension = source_url.split(".")[-1].lower()
+
# Name of the video file to add to the URL (if necessary)
video_file_add = ""
if extension not in VIDEO_ALLOWED_EXTENSIONS:
@@ -480,21 +528,28 @@ def upload_bbb_recording_to_pod(request, record_id):
# Extension overload
extension = video_file_add.split(".")[-1].lower()
- # Verify that video exists and not oversised
+ # Verify that video exists and not oversized
source_video_url = manage_recording_url(source_url, video_file_add)
verify_video_exists_and_size(source_video_url)
# Step 2: Define destination source file
extension = source_video_url.split(".")[-1].lower()
- dest_file = define_dest_file(request, recording.id, extension)
- dest_path = define_dest_path(request, recording.id, extension)
+ dest_file, dest_path = define_dest_file_and_path(
+ recording.owner,
+ recording.id,
+ extension
+ )
# Step 3: Download the video file
- source_video_url = manage_download(session, source_url, video_file_add, dest_file)
+ source_video_url = manage_download(
+ session,
+ source_url,
+ video_file_add,
+ dest_file
+ )
# Step 4: Save informations about the recording
- recording_title = request.POST.get("recording_name")
- save_external_recording(request, record_id)
+ save_external_recording(recording.owner, record_id)
# Step 5: Save and encode Pod video
description = _(
@@ -502,27 +557,55 @@ def upload_bbb_recording_to_pod(request, record_id):
'%(url)s'
) % {"type": recording.get_type_display(), "url": source_video_url}
- save_video(request, dest_path, recording_title, description)
+ save_video(recording.owner, dest_path, recording.name, description)
return True
except Exception as exc:
- msg = {}
- msg["error"] = _("Impossible to upload to Pod the video")
- try:
- # Management of error messages from sub-functions
- message = "%s %s" % (exc.args[0]["error"], exc.args[0]["message"])
- except Exception:
- # Management of error messages in all cases
- message = str(exc)
+ manage_standard_exception(exc)
- msg["message"] = mark_safe(message)
- msg["proposition"] = _(
- "Try changing the record type or address for this recording."
- )
- raise ValueError(msg)
+def upload_local_video_recording_to_pod(record_id: id, dest_file: str, dest_path: str):
+ """Upload a local (typically in Pod filesystem) video file recording to Pod.
+ Useful for video files that have been encoded following
+ the recording of a BBB presentation.
+
+ Args:
+ record_id (Integer): id record in the database
+ dest_file (String): destination filename
+ dest_path (String): destination path
+
+ Raises:
+ ValueError: exception raised if no video found in the filesystem
+
+ Returns:
+ Boolean: True if upload achieved
+ """
+ try:
+ recording = ExternalRecording.objects.get(id=record_id)
+
+ # Step 1: Save informations about the recording
+ save_external_recording(recording.owner, record_id)
+
+ # Step 2: Save and encode Pod video
+ description = _(
+ "This video was uploaded to Pod; its origin is %(type)s: "
+ '%(url)s'
+ ) % {"type": recording.get_type_display(), "url": recording.source_url}
+
+ save_video(recording.owner, dest_path, recording.name, description)
+
+ log.info("- Video file uploaded: %s" % dest_file)
+
+ return True
+ except Exception as exc:
+ manage_standard_exception(exc)
-def upload_mediacad_recording_to_pod(request, record_id, type_source_url):
+
+def upload_mediacad_recording_to_pod(
+ request,
+ record_id: int,
+ type_source_url: TypeSourceURL
+):
"""Upload a Mediacad video file recording to Pod.
Args:
@@ -551,40 +634,30 @@ def upload_mediacad_recording_to_pod(request, record_id, type_source_url):
verify_video_exists_and_size(source_video_url)
# Step 2: Define destination source file
- dest_file = define_dest_file(request, recording.id, extension)
- dest_path = define_dest_path(request, recording.id, extension)
+ dest_file, dest_path = define_dest_file_and_path(
+ recording.owner,
+ recording.id,
+ extension
+ )
# Step 3: Download the video file
download_video_file(session, source_video_url, dest_file)
# Step 4: Save informations about the recording
recording_title = request.POST.get("recording_name")
- save_external_recording(request, record_id)
+ save_external_recording(recording.owner, record_id)
# Step 5: Save and encode Pod video
# Get description from JSON API
description = get_mediacad_api_description(type_source_url)
- save_video(request, dest_path, recording_title, description)
+ save_video(recording.owner, dest_path, recording_title, description)
return True
except Exception as exc:
- msg = {}
- msg["error"] = _("Impossible to upload to Pod the video")
- try:
- # Management of error messages from sub-functions
- message = "%s %s" % (exc.args[0]["error"], exc.args[0]["message"])
- except Exception:
- # Management of error messages in all cases
- message = str(exc)
-
- msg["message"] = mark_safe(message)
- msg["proposition"] = _(
- "Try changing the record type or address for this recording."
- )
- raise ValueError(msg)
+ manage_standard_exception(exc)
-def get_mediacad_api_description(type_source_url):
+def get_mediacad_api_description(type_source_url: TypeSourceURL) -> str:
"""Returns description of a Mediacad video, after a call to Mediacad JSON API.
Args:
@@ -618,7 +691,7 @@ def get_mediacad_api_description(type_source_url):
return description
-def upload_youtube_recording_to_pod(request, record_id):
+def upload_youtube_recording_to_pod(request, record_id: int):
"""Upload Youtube recording to Pod.
Use PyTube with its API
@@ -675,7 +748,7 @@ def upload_youtube_recording_to_pod(request, record_id):
yt_stream.download(dest_dir, filename=filename)
# Step 4: Save informations about the recording
- save_external_recording(request, record_id)
+ save_external_recording(request.user, record_id)
# Step 5: Save and encode Pod video
description = _(
@@ -683,7 +756,7 @@ def upload_youtube_recording_to_pod(request, record_id):
'its origin is Youtube: %(url)s'
) % {"name": yt_video.title, "url": source_url}
recording_title = request.POST.get("recording_name")
- save_video(request, dest_path, recording_title, description, date_evt)
+ save_video(request.user, dest_path, recording_title, description, date_evt)
return True
except VideoUnavailable:
@@ -707,23 +780,10 @@ def upload_youtube_recording_to_pod(request, record_id):
msg["proposition"] = _("Try changing the address of this recording.")
raise ValueError(msg)
except Exception as exc:
- msg = {}
- msg["error"] = _("Impossible to upload to Pod the video")
- try:
- # Management of error messages from sub-functions
- message = "%s %s" % (exc.args[0]["error"], exc.args[0]["message"])
- except Exception:
- # Management of error messages in all cases
- message = str(exc)
-
- msg["message"] = mark_safe(message)
- msg["proposition"] = _(
- "Try changing the record type or address for this recording."
- )
- raise ValueError(msg)
+ manage_standard_exception(exc)
-def upload_peertube_recording_to_pod(request, record_id): # noqa: C901
+def upload_peertube_recording_to_pod(request, record_id: int) -> bool: # noqa: C901
"""Upload Peertube recording to Pod.
More information: https://docs.joinpeertube.org/api/rest-getting-started
@@ -812,15 +872,18 @@ def upload_peertube_recording_to_pod(request, record_id): # noqa: C901
# Step 2: Define destination source file
extension = source_video_url.split(".")[-1].lower()
- dest_file = define_dest_file(request, pt_video_uuid, extension)
- dest_path = define_dest_path(request, pt_video_uuid, extension)
+ dest_file, dest_path = define_dest_file_and_path(
+ request.user,
+ pt_video_uuid,
+ extension
+ )
# Step 3: Download the video file
download_video_file(session, source_video_url, dest_file)
# Step 4: Save informations about the recording
recording_title = request.POST.get("recording_name")
- save_external_recording(request, record_id)
+ save_external_recording(request.user, record_id)
# Step 5: Save and encode Pod video
description = _(
@@ -828,27 +891,215 @@ def upload_peertube_recording_to_pod(request, record_id): # noqa: C901
"%(url)s."
) % {"name": pt_video_name, "url": pt_video_url}
description = ("%s %s") % (description, pt_video_description)
- save_video(request, dest_path, recording_title, description, date_evt)
+ save_video(request.user, dest_path, recording_title, description, date_evt)
return True
except Exception as exc:
- msg = {}
- msg["error"] = _("Impossible to upload to Pod the PeerTube video")
- try:
- # Management of error messages from sub-functions
- message = "%s %s" % (exc.args[0]["error"], exc.args[0]["message"])
- except Exception:
- # Management of error messages in all cases
- message = str(exc)
+ manage_standard_exception(exc)
- msg["message"] = mark_safe(message)
- msg["proposition"] = _(
- "Try changing the record type or address for this recording."
+
+def start_bbb_encode_presentation_and_upload_to_pod(
+ record_id: int,
+ url: str,
+ extension: str
+):
+ """Send an asynchronous task or a thread to encode a BBB presentation
+ into a video file and upload it to Pod.
+
+ With Celery, logs can be found in encoding servers, worker.log.
+ Without Celery, a thread is run to do the job.
+ """
+ if CELERY_TO_ENCODE:
+ task_start_bbb_presentation_encode_and_upload_to_pod.delay(
+ record_id,
+ url,
+ extension
)
- raise ValueError(msg)
+ else:
+ log.info(
+ "START BBB ENCODE PRESENTATION/UPLOAD FOR EXTERNAL RECORDING %s" % record_id
+ )
+ # Thread use
+ t = threading.Thread(
+ target=bbb_encode_presentation_and_upload_to_pod,
+ args=[record_id, url, extension]
+ )
+ t.setDaemon(True)
+ t.start()
+
+
+def start_bbb_encode_presentation_and_move_to_destination(
+ filename: str,
+ url: str,
+ dest_file: str
+):
+ """Send an asynchronous task to encode or encode direclty a BBB presentation
+ into a video file and move it to a specific directory.
+
+ With Celery, logs can be found in encoding servers, worker.log.
+ Without Celery, we didn't use a thread because it caused problems
+ for the script importing old BBB records.
+ """
+ if CELERY_TO_ENCODE:
+ task_start_bbb_presentation_encode_and_move_to_destination.delay(
+ filename,
+ url,
+ dest_file
+ )
+ else:
+ log.info("START BBB ENCODE PRESENTATION/MOVE FOR %s" % filename)
+ # No thread here
+ bbb_encode_presentation_and_move_to_destination(filename, url, dest_file)
-def get_stateless_recording(request, data):
+def bbb_encode_presentation(filename: str, url: str) -> str:
+ """Encode a BBB presentation into a video file.
+
+ Use bbb-recorder github project to do this.
+ The logs for this operation can be found in
+ IMPORT_VIDEO_BBB_RECORDER_PATH/logs/record_id.log.
+ The video file is generated in IMPORT_VIDEO_BBB_RECORDER_PATH/.
+ """
+ # Logs
+ logname = "%s.log" % filename
+ logpath = os.path.join(
+ IMPORT_VIDEO_BBB_RECORDER_PATH,
+ "logs",
+ logname
+ )
+
+ # The command looks like:
+ # node export.js https://bbb.univ.fr/playback/presentation/2.0/
+ # playback.html?meetingId=INTERNAL_MEETING_ID INTERNAL_MEETING_ID.webm
+ # > /data/www/USERPOD/bbb-recorder/logs/INTERNAL_MEETING_ID.log
+ # 2>&1 < /dev/null
+ # Recording_URL can be also like https://bbb.univ.fr/playback/presentation/2.3/
+ # INTERNAL_MEETING_ID/?meetingId=INTERNAL_MEETING_ID/
+ # Encode the presentation in webm (not mp4, less data in log)
+ # Put the generated video file on the bbb-recorder plugin directory
+ command = "cd %s; node export.js %s %s > %s 2>&1 < /dev/null" % (
+ IMPORT_VIDEO_BBB_RECORDER_PLUGIN,
+ url,
+ filename,
+ logpath
+ )
+ log.info("- bbb_encode_presentation")
+ log.info("- Command: %s" % command)
+ log.info("- Start encoding: %s" % time.ctime())
+
+ # Execute the process
+ result = subprocess.run(
+ command,
+ shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT
+ )
+ log.info("- Process result: %s" % result)
+ log.info("- End encoding: %s" % time.ctime())
+
+ source_generated_file = os.path.join(
+ IMPORT_VIDEO_BBB_RECORDER_PATH,
+ filename,
+ )
+ log.info("- Generated file: %s" % source_generated_file)
+ # returns the path and filename of the generated video file
+ return source_generated_file
+
+
+def bbb_encode_presentation_and_move_to_destination(
+ filename: str,
+ url: str,
+ dest_file: str
+):
+ """Encode a BBB presentation into a video file and put it on a specific directory.
+
+ Use bbb-recorder github project to do this.
+ The logs for this operation can be found in IMPORT_VIDEO_BBB_RECORDER_PATH/logs/.log.
+ The video file is generated in IMPORT_VIDEO_BBB_RECORDER_PATH/ then moved
+ to the destination (dest_file).
+ """
+ # Achieve the encode of the presentation via bbb-recorder
+ source_generated_file = bbb_encode_presentation(filename, url)
+
+ if check_file_exists(source_generated_file):
+ log.info("- Video file generated: %s" % source_generated_file)
+
+ # Move generated video file into the good directory
+ move_file(source_generated_file, dest_file)
+
+ log.info("- Video file moved to: %s" % dest_file)
+ else:
+ log.error("- Video file not generated: %s" % source_generated_file)
+
+
+def bbb_encode_presentation_and_upload_to_pod(record_id: int, url: str, extension: str):
+ """Encode a BBB presentation into a video file and upload it to Pod.
+
+ Use bbb-recorder github project to do this.
+ The logs for this operation can be found in
+ IMPORT_VIDEO_BBB_RECORDER_PATH/logs/record_id.log.
+ The video file is generated in IMPORT_VIDEO_BBB_RECORDER_PATH/ then moved
+ to the user directory and finally saved and encoded in Pod.
+ """
+ # Get the record
+ recording = ExternalRecording.objects.get(id=record_id)
+ filename = str(record_id) + "." + extension
+
+ dest_file, dest_path = define_dest_file_and_path(
+ recording.owner,
+ recording.id,
+ extension
+ )
+ # Change the recording state
+ recording.state = _(
+ "Convert web presentation to video in progress..."
+ )
+ recording.save()
+
+ # Achieve the encode of the presentation via bbb-recorder
+ source_generated_file = bbb_encode_presentation(filename, url)
+
+ if check_file_exists(source_generated_file):
+ log.info("- Video file generated: %s" % source_generated_file)
+
+ # Move generated video file into the good directory
+ move_file(source_generated_file, dest_file)
+
+ log.info("- Video file moved to: %s" % dest_file)
+
+ # Finally upload the generated video file to Pod
+ if upload_local_video_recording_to_pod(record_id, dest_file, dest_path):
+ recording.state = _("Video file already uploaded to Pod")
+ recording.save()
+
+ else:
+ # Video file not generated : inform the user via the recording state
+ recording.state = _(
+ "Impossible to upload to Pod the video, "
+ "the link provided does not seem valid."
+ )
+ recording.save()
+
+
+def manage_standard_exception(exc: Exception):
+ """Manage standard exception to raise a specific error."""
+ msg = {}
+ msg["error"] = _("Impossible to upload to Pod the video")
+ try:
+ # Management of error messages from sub-functions
+ message = "%s %s" % (exc.args[0]["error"], exc.args[0]["message"])
+ except Exception:
+ # Management of error messages in all cases
+ message = str(exc)
+
+ msg["message"] = mark_safe(message)
+ msg["proposition"] = _(
+ "Try changing the record type or address for this recording."
+ )
+ raise ValueError(msg)
+
+
+def get_stateless_recording(request, data: ExternalRecording):
"""Return a stateless recording from an external recording.
Args:
@@ -858,7 +1109,7 @@ def get_stateless_recording(request, data):
Returns:
StatelessRecording: stateless recording
"""
- recording = StatelessRecording(data.id, data.name, "published")
+ recording = StatelessRecording(data.id, data.name, None)
# By default, upload to Pod is possible
recording.canUpload = True
# Only owner can delete this external recording
@@ -868,33 +1119,29 @@ def get_stateless_recording(request, data):
recording.uploadedToPodBy = data.uploaded_to_pod_by
- # State management
- if recording.uploadedToPodBy is None:
- recording.state = _("Video file not uploaded to Pod")
- else:
- recording.state = _("Video file already uploaded to Pod")
+ # Status management
+ recording.state = get_status_recording(data)
# Management of the external recording type
if data.type == "bigbluebutton":
# Manage BBB recording URL
- video_url = data.source_url
- # For BBB, external URL can be the video or presentation playback
- if video_url.find("playback/video") != -1:
- # Management for standards video URLs with BBB or Scalelite server
- recording.videoUrl = video_url
- elif video_url.find("playback/presentation/2.3") != -1:
- # Management for standards presentation URLs with BBB or Scalelite server
- # Add computed video playback
- recording.videoUrl = video_url.replace(
- "playback/presentation/2.3", "playback/video"
- )
- recording.presentationUrl = video_url
+ # Management for old presentation URLs with BBB or Scalelite server
+ if data.source_url.find("/playback/presentation/2.0/playback.html?") != -1:
+ recording.presentationUrl = data.source_url
+ # Management for standard presentation URLs with BBB or Scalelite server
+ elif data.source_url.find("/playback/presentation/2.3/") != -1:
+ recording.presentationUrl = data.source_url
+ # Management of other situations
else:
- # Management of other situations, non standards URLs
- recording.videoUrl = video_url
-
- # For old BBB or BBB 2.6+ without video playback
- if check_file_exists(recording.videoUrl) is False:
+ recording.videoUrl = data.source_url
+
+ # If possible to encode BBB presentation
+ # Useful for old BBB or BBB 2.6+ without video playback
+ if USE_IMPORT_VIDEO_BBB_RECORDER and recording.presentationUrl != "":
+ recording.canUpload = True
+ recording.videoUrl = recording.presentationUrl
+ # In all case
+ if recording.videoUrl == "" or check_url_exists(recording.videoUrl) is False:
recording.state = _(
"No video file found. Upload to Pod as a video is not possible."
)
@@ -908,3 +1155,14 @@ def get_stateless_recording(request, data):
recording.type = data.get_type_display
return recording
+
+
+def get_status_recording(data: ExternalRecording) -> str:
+ """Get the status of an external recording."""
+ if data.uploaded_to_pod_by is None and data.state is None:
+ state = _("Video file not uploaded to Pod")
+ elif data.uploaded_to_pod_by is not None and data.state is None:
+ state = _("Video file already uploaded to Pod")
+ else:
+ state = data.state
+ return state
diff --git a/pod/locale/fr/LC_MESSAGES/django.po b/pod/locale/fr/LC_MESSAGES/django.po
index a4789b6812..5e03a9fc4f 100644
--- a/pod/locale/fr/LC_MESSAGES/django.po
+++ b/pod/locale/fr/LC_MESSAGES/django.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Pod\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-15 10:00+0000\n"
+"POT-Creation-Date: 2024-02-15 17:43+0100\n"
"PO-Revision-Date: \n"
"Last-Translator: ptitloup \n"
"Language-Team: Pod Team cotech-esup-pod@esup-portail.org\n"
@@ -1091,26 +1091,6 @@ msgstr "Les chapitres ne peuvent commencer au même moment."
msgid "You must save your chapters to view the result."
msgstr "Vous devez sauvegarder vos chapitres pour voir le résultat."
-#: pod/chapter/templates/video_chapter.html
-#: pod/live/templates/live/event_edit.html
-#: pod/live/templates/live/event_immediate_edit.html
-#: pod/main/templates/contact_us.html
-#: pod/video/templates/channel/channel_edit.html
-#: pod/video/templates/channel/theme_edit.html
-#: pod/video/templates/videos/video_edit.html
-msgid "Mandatory fields"
-msgstr "Champs obligatoires"
-
-#: pod/chapter/templates/video_chapter.html
-#: pod/live/templates/live/event_edit.html
-#: pod/live/templates/live/event_immediate_edit.html
-#: pod/main/templates/contact_us.html
-#: pod/video/templates/channel/channel_edit.html
-#: pod/video/templates/channel/theme_edit.html
-#: pod/video/templates/videos/video_edit.html
-msgid "Fields marked with an asterisk are mandatory."
-msgstr "Les champs marqués avec un astérisque sont obligatoires."
-
#: pod/chapter/views.py
msgid "You cannot chapter this video."
msgstr "Vous ne pouvez chapitrer cette vidéo."
@@ -2557,6 +2537,10 @@ msgstr ""
"Veuillez entrer l’adresse de l’enregistrement à télécharger. Cette adresse "
"doit correspondre au type d’enregistrement sélectionné."
+#: pod/import_video/models.py
+msgid "Recording state"
+msgstr "Etat de l’enregistrement"
+
#: pod/import_video/models.py pod/meeting/models.py
msgid "User who uploaded to Pod the video file"
msgstr "Utilisateur qui a téléversé le fichier vidéo sur Pod"
@@ -2854,6 +2838,13 @@ msgstr "Impossible de téléverser la vidéo sur Pod"
msgid "YouTube content is inaccessible."
msgstr "Ce contenu de YouTube est inaccessible."
+#: pod/import_video/tests/test_views.py pod/import_video/views.py
+msgid ""
+"Impossible to upload to Pod the video, the link provided does not seem valid."
+msgstr ""
+"Impossible de télécharger sur Pod la vidéo, le lien fourni ne semble pas "
+"valide."
+
#: pod/import_video/utils.py
msgid "No URL found / No recording name"
msgstr "Aucune URL trouvée / Aucun nom d’enregistrement"
@@ -3048,25 +3039,31 @@ msgstr ""
"PeerTube : %(url)s."
#: pod/import_video/views.py
-msgid "Impossible to upload to Pod the PeerTube video"
-msgstr "Impossible de téléverser la vidéo PeerTube sur Pod"
-
-#: pod/import_video/views.py
-#: pod/meeting/templates/meeting/internal_recordings.html
-msgid "Video file not uploaded to Pod"
-msgstr "Le fichier vidéo n’a pas été téléversé sur Pod"
+msgid "Convert web presentation to video in progress..."
+msgstr "Conversion de la présentation web en vidéo en cours..."
#: pod/import_video/views.py
#: pod/meeting/templates/meeting/internal_recordings.html
msgid "Video file already uploaded to Pod"
msgstr "Fichier vidéo déjà téléversé sur Pod"
+#: pod/import_video/views.py pod/meeting/views.py
+msgid "Try changing the record type or address for this recording."
+msgstr ""
+"Essayez de modifier le type d’enregistrement ou l’adresse de cet "
+"enregistrement."
+
#: pod/import_video/views.py
msgid "No video file found. Upload to Pod as a video is not possible."
msgstr ""
"Aucun fichier vidéo trouvé. Le téléversement sur Pod en tant que vidéo n’est "
"pas possible."
+#: pod/import_video/views.py
+#: pod/meeting/templates/meeting/internal_recordings.html
+msgid "Video file not uploaded to Pod"
+msgstr "Le fichier vidéo n’a pas été téléversé sur Pod"
+
#: pod/live/admin.py pod/live/models.py
msgid "QR code to record immediately an event"
msgstr "QR code pour la programmation d’un évènement immédiat"
@@ -4991,6 +4988,14 @@ msgstr "Vous venez d’envoyer un message depuis"
msgid "Regards"
msgstr "Cordialement"
+#: pod/main/templates/main/mandatory_fields.html
+msgid "Mandatory fields"
+msgstr "Champs obligatoires"
+
+#: pod/main/templates/main/mandatory_fields.html
+msgid "Fields marked with an asterisk are mandatory."
+msgstr "Les champs marqués avec un astérisque sont obligatoires."
+
#: pod/main/templates/maintenance.html
msgid ""
"The maintenance is over, you can go back to work. Thank you for your "
@@ -7150,6 +7155,10 @@ msgstr "Définir comme brouillon"
msgid "Transcript selected"
msgstr "Transcrire la sélection"
+#: pod/video/admin.py
+msgid "Change video owner"
+msgstr "Changer le propriétaire de vidéos"
+
#: pod/video/forms.py
msgid "File field"
msgstr "Fichier"
@@ -8107,10 +8116,6 @@ msgstr "Supprimer la catégorie"
msgid "Permanently delete your category?"
msgstr "Supprimer définitivement votre catégorie ?"
-#: pod/video/templates/videos/change_video_owner.html
-msgid "Change video owner"
-msgstr "Changer le propriétaire de vidéos"
-
#: pod/video/templates/videos/change_video_owner.html
msgid "Old owner"
msgstr "Ancien propriétaire"
@@ -9251,30 +9256,6 @@ msgstr "Résultats de la recherche"
msgid "Esup-Pod xAPI"
msgstr "xAPI Esup-Pod"
-#~ msgid "Use this link to share the video in draft mode"
-#~ msgstr "Utiliser ce lien pour partager la vidéo en mode brouillon"
-
-#~ msgid "YouTube content is inaccessible"
-#~ msgstr "Le contenu YouTube n’est pas disponible"
-
-#~ msgid ""
-#~ "YouTube content is inaccessible. This content does not appear to be "
-#~ "publicly available."
-#~ msgstr ""
-#~ "Le contenu YouTube est inaccessible. Ce contenu ne semble pas être "
-#~ "disponible publiquement."
-
-#~ msgid ""
-#~ "Access to adding external recording has been restricted. If you want to "
-#~ "add external recordings on the platform, please"
-#~ msgstr ""
-#~ "L’accès à l’ajout d’un enregistrement externe a été restreint. Si vous "
-#~ "souhaitez ajouter des enregistrements externes sur la plateforme, s’il "
-#~ "vous plaît"
-
-#~ msgid "Deleting the record"
-#~ msgstr "Supprimer cet enregistrement"
-
#~ msgid "Playlist image"
#~ msgstr "Image de la playlist"
diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po
index 64dfced9b5..fe3862991f 100644
--- a/pod/locale/fr/LC_MESSAGES/djangojs.po
+++ b/pod/locale/fr/LC_MESSAGES/djangojs.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Esup-Pod\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-15 10:00+0000\n"
+"POT-Creation-Date: 2024-02-15 17:43+0100\n"
"PO-Revision-Date: \n"
"Last-Translator: obado \n"
"Language-Team: \n"
diff --git a/pod/locale/nl/LC_MESSAGES/django.po b/pod/locale/nl/LC_MESSAGES/django.po
index 934c0473cd..df58e7ec83 100644
--- a/pod/locale/nl/LC_MESSAGES/django.po
+++ b/pod/locale/nl/LC_MESSAGES/django.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Pod\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-15 10:00+0000\n"
+"POT-Creation-Date: 2024-02-15 17:43+0100\n"
"PO-Revision-Date: 2023-06-08 14:37+0200\n"
"Last-Translator: obado \n"
"Language-Team: \n"
@@ -1028,26 +1028,6 @@ msgstr ""
msgid "You must save your chapters to view the result."
msgstr ""
-#: pod/chapter/templates/video_chapter.html
-#: pod/live/templates/live/event_edit.html
-#: pod/live/templates/live/event_immediate_edit.html
-#: pod/main/templates/contact_us.html
-#: pod/video/templates/channel/channel_edit.html
-#: pod/video/templates/channel/theme_edit.html
-#: pod/video/templates/videos/video_edit.html
-msgid "Mandatory fields"
-msgstr ""
-
-#: pod/chapter/templates/video_chapter.html
-#: pod/live/templates/live/event_edit.html
-#: pod/live/templates/live/event_immediate_edit.html
-#: pod/main/templates/contact_us.html
-#: pod/video/templates/channel/channel_edit.html
-#: pod/video/templates/channel/theme_edit.html
-#: pod/video/templates/videos/video_edit.html
-msgid "Fields marked with an asterisk are mandatory."
-msgstr ""
-
#: pod/chapter/views.py
msgid "You cannot chapter this video."
msgstr ""
@@ -2433,6 +2413,10 @@ msgid ""
"match the record type selected."
msgstr ""
+#: pod/import_video/models.py
+msgid "Recording state"
+msgstr ""
+
#: pod/import_video/models.py pod/meeting/models.py
msgid "User who uploaded to Pod the video file"
msgstr ""
@@ -2687,6 +2671,11 @@ msgstr ""
msgid "YouTube content is inaccessible."
msgstr ""
+#: pod/import_video/tests/test_views.py pod/import_video/views.py
+msgid ""
+"Impossible to upload to Pod the video, the link provided does not seem valid."
+msgstr ""
+
#: pod/import_video/utils.py
msgid "No URL found / No recording name"
msgstr ""
@@ -2781,10 +2770,6 @@ msgid ""
"target=\"_blank\">%(url)s"
msgstr ""
-#: pod/import_video/views.py pod/meeting/views.py
-msgid "Try changing the record type or address for this recording."
-msgstr ""
-
#: pod/import_video/views.py
#, python-format
msgid ""
@@ -2856,23 +2841,27 @@ msgid ""
msgstr ""
#: pod/import_video/views.py
-msgid "Impossible to upload to Pod the PeerTube video"
+msgid "Convert web presentation to video in progress..."
msgstr ""
#: pod/import_video/views.py
#: pod/meeting/templates/meeting/internal_recordings.html
-msgid "Video file not uploaded to Pod"
+msgid "Video file already uploaded to Pod"
msgstr ""
-#: pod/import_video/views.py
-#: pod/meeting/templates/meeting/internal_recordings.html
-msgid "Video file already uploaded to Pod"
+#: pod/import_video/views.py pod/meeting/views.py
+msgid "Try changing the record type or address for this recording."
msgstr ""
#: pod/import_video/views.py
msgid "No video file found. Upload to Pod as a video is not possible."
msgstr ""
+#: pod/import_video/views.py
+#: pod/meeting/templates/meeting/internal_recordings.html
+msgid "Video file not uploaded to Pod"
+msgstr ""
+
#: pod/live/admin.py pod/live/models.py
msgid "QR code to record immediately an event"
msgstr ""
@@ -4727,6 +4716,14 @@ msgstr ""
msgid "Regards"
msgstr ""
+#: pod/main/templates/main/mandatory_fields.html
+msgid "Mandatory fields"
+msgstr ""
+
+#: pod/main/templates/main/mandatory_fields.html
+msgid "Fields marked with an asterisk are mandatory."
+msgstr ""
+
#: pod/main/templates/maintenance.html
msgid ""
"The maintenance is over, you can go back to work. Thank you for your "
@@ -6735,6 +6732,10 @@ msgstr ""
msgid "Transcript selected"
msgstr ""
+#: pod/video/admin.py
+msgid "Change video owner"
+msgstr ""
+
#: pod/video/forms.py
msgid "File field"
msgstr ""
@@ -7597,10 +7598,6 @@ msgstr ""
msgid "Permanently delete your category?"
msgstr ""
-#: pod/video/templates/videos/change_video_owner.html
-msgid "Change video owner"
-msgstr ""
-
#: pod/video/templates/videos/change_video_owner.html
msgid "Old owner"
msgstr ""
@@ -7647,10 +7644,8 @@ msgid "Choose an action"
msgstr ""
#: pod/video/templates/videos/dashboard.html
-#, fuzzy
-#| msgid "Title field"
msgid "Edit a field"
-msgstr "Titel veld"
+msgstr ""
#: pod/video/templates/videos/dashboard.html
msgid "Cursus"
@@ -8697,8 +8692,3 @@ msgstr ""
#: pod/xapi/apps.py
msgid "Esup-Pod xAPI"
msgstr ""
-
-#, fuzzy
-#~| msgid "Email sent?"
-#~ msgid "Email addresse"
-#~ msgstr "Email verzonden?"
diff --git a/pod/locale/nl/LC_MESSAGES/djangojs.po b/pod/locale/nl/LC_MESSAGES/djangojs.po
index 728800221b..44034858f7 100644
--- a/pod/locale/nl/LC_MESSAGES/djangojs.po
+++ b/pod/locale/nl/LC_MESSAGES/djangojs.po
@@ -5,7 +5,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Esup-Pod\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2024-02-15 10:00+0000\n"
+"POT-Creation-Date: 2024-02-15 17:43+0100\n"
"PO-Revision-Date: 2023-02-08 15:22+0100\n"
"Last-Translator: obado \n"
"Language-Team: \n"
diff --git a/pod/main/configuration.json b/pod/main/configuration.json
index 2b759e6213..1752dcb985 100644
--- a/pod/main/configuration.json
+++ b/pod/main/configuration.json
@@ -899,10 +899,55 @@
},
"pod_version_end": "",
"pod_version_init": "3.3.0"
+ },
+ "USE_IMPORT_VIDEO_BBB_RECORDER": {
+ "default_value": false,
+ "description": {
+ "en": [
+ "Using the bbb-recorder plugin for the video import module; ",
+ "useful for converting a BigBlueButton presentation into a video file."
+ ],
+ "fr": [
+ "Utilisation du plugin bbb-recorder pour le module import-vidéo; ",
+ "utile pour convertir une présentation BigBlueButton en fichier vidéo."
+ ]
+ },
+ "pod_version_end": "",
+ "pod_version_init": "3.5.1"
+ },
+ "IMPORT_VIDEO_BBB_RECORDER_PLUGIN": {
+ "default_value": "/home/pod/bbb-recorder/",
+ "description": {
+ "en": [
+ "bbb-recorder plugin directory (see https://github.com/jibon57/bbb-recorder documentation).",
+ "bbb-recorder must be installed in this directory, on all encoding servers.",
+ "bbb-recorder creates a Downloads directory, at the same level, which requires disk space."
+ ],
+ "fr": [
+ "Répertoire du plugin bbb-recorder (voir la documentation https://github.com/jibon57/bbb-recorder).",
+ "bbb-recorder doit être installé dans ce répertoire, sur tous les serveurs d'encodage.",
+ "bbb-recorder crée un répertoire Downloads, au même niveau, qui nécessite de l'espace disque."
+ ]
+ },
+ "pod_version_end": "",
+ "pod_version_init": "3.5.1"
+ },
+ "IMPORT_VIDEO_BBB_RECORDER_PATH": {
+ "default_value": true,
+ "description": {
+ "en": [
+ "Directory that will contain the video files generated by bbb-recorder."
+ ],
+ "fr": [
+ "Répertoire qui contiendra les fichiers vidéo générés par bbb-recorder."
+ ]
+ },
+ "pod_version_end": "",
+ "pod_version_init": "3.5.1"
}
},
"title": {
- "en": "",
+ "en": "Video import application configuration",
"fr": "Configuration application d'import vidéo"
}
},
diff --git a/pod/main/tasks.py b/pod/main/tasks.py
index 4d3e6caf6a..580102c34a 100644
--- a/pod/main/tasks.py
+++ b/pod/main/tasks.py
@@ -65,3 +65,31 @@ def task_end_live_transcription(self, slug):
if running_task:
self.app.control.revoke(running_task.task_id, terminate=True)
running_task.delete()
+
+
+@app.task(bind=True)
+def task_start_bbb_presentation_encode_and_upload_to_pod(
+ self,
+ record_id: int,
+ url: str,
+ extension: str
+):
+ """Start BBB presentation encoding with Celery, then upload to Pod."""
+ print("CELERY START BBB ENCODE PRESENTATION/UPLOAD RECORD ID %s" % record_id)
+ from pod.import_video.views import bbb_encode_presentation_and_upload_to_pod
+
+ bbb_encode_presentation_and_upload_to_pod(record_id, url, extension)
+
+
+@app.task(bind=True)
+def task_start_bbb_presentation_encode_and_move_to_destination(
+ self,
+ filename: str,
+ url: str,
+ dest_file: str
+):
+ """Start BBB presentation encoding with Celery, then move the video file."""
+ print("CELERY START BBB ENCODE PRESENTATION/MOVE %s" % filename)
+ from pod.import_video.views import bbb_encode_presentation_and_move_to_destination
+
+ bbb_encode_presentation_and_move_to_destination(filename, url, dest_file)
diff --git a/pod/meeting/views.py b/pod/meeting/views.py
index 954da9cae3..9809653dec 100644
--- a/pod/meeting/views.py
+++ b/pod/meeting/views.py
@@ -1247,7 +1247,7 @@ def upload_bbb_recording_to_pod(request, record_id, meeting_id):
'%(url)s'
) % {"type": "Big Blue Button", "url": source_video_url}
- save_video(request, dest_path, recording_title, description)
+ save_video(request.user, dest_path, recording_title, description)
return True
except Exception as exc:
diff --git a/pod/video/management/commands/migrate_bbb_recordings.py b/pod/video/management/commands/migrate_bbb_recordings.py
new file mode 100644
index 0000000000..eb153e547b
--- /dev/null
+++ b/pod/video/management/commands/migrate_bbb_recordings.py
@@ -0,0 +1,876 @@
+"""
+Useful script for migrating BBB records when changing BBB
+infrastructure. Typically, when moving from a local architecture to the
+French Ministry of Higher Education and Research (ESR) BBB architecture.
+
+More information on this module at: https://www.esup-portail.org/wiki/x/C4CFUQ
+
+Reminder of constraints :
+ - no use of Pod's old BBB module (this module will be phased out in the near future)
+ - problem with the BBB API: moderators are only available when the BBB session
+ session is in progress. After that, the information is no longer available in BBB.
+ This information is present in the BBB client, i.e. Pod or Moodle
+ (or Greenlight...).
+ - by default, it is possible to reconstruct a BBB record in the BBB infrastructure
+ (typically to obtain the recording in video format) only if the raw files
+ are still present.
+ By default, these raw files are deleted after 14 days.
+
+Principle :
+ - add recording management to the external video import module,
+ BBB, presentation type. This means using the Github project bbb-recorder;
+ in any case, there's no choice for this migration.
+ You need to install bbb-recorder on the encoding servers and set up
+ IMPORT_VIDEO_BBB_RECORDER_PLUGIN and IMPORT_VIDEO_BBB_RECORDER_PATH correctly.
+ This feature has been added in Esup-Pod 3.5.1.
+
+ - create this migration script, which offers several possibilities:
+
+ * 1° option, for those who have few recordings to recover
+
+This script will convert presentations from the old BBB architecture into video files
+(as before, via the bbb-recorder plugin) and put these files in the directory
+of a recorder used to claim recordings.
+Of course, if there are already video presentations, the video file will be copied
+directly.
+Once all the videos have been encoded, the local BBB architecture can be stopped.
+This is made possible by using the --use-manual-claim parameter and the
+setup directly in this file.
+Please note that, depending on your Pod architecture, encoding will be performed
+either via Celery tasks or directly, one after the other. Don't hesitate to test
+on a few recordings first and run this script in the background (use &).
+
+ * 2nd option, for those who have a lot of recordings to retrieve
+
+The idea is to give users time to choose which records they wish to keep
+(it's not possible or useful to convert everything).
+To do this, you'll need to leave the old BBB/Scalelite server open at least
+for a few months (just to access the records).
+On the scripting side, you'll need access to the Moodle database to know
+who did what.
+So, for each BBB recording, the script would create a line in Pod's
+"My external videos" module, of type BBB, for the moderators.
+If these moderators have never logged into Pod, they will be created in Pod.
+They can then import these recordings into Pod themselves.
+Just in case, if records are not identifiable, they will be associated
+to an administrator. In this way, for records from sources other than Pod or Moodle,
+they will automatically be associated with an administrator (unless
+this script is modified).
+It is also planned (if access to the Moodle database is write-only, of course) to add
+information directly to the BBB session in Moodle (intro field).
+This is made possible by using the --use-import-video parameter,
+the --use-database-moodle parameter and setup directly in this file.
+This script has already been tested with Moodle 4.
+
+
+This script also allows you to :
+ - simulate what will be done via the --dry parameter
+ - process only certain lines via the --min-value-record-process
+ and --max-value-record-process parameters.
+
+Examples and use cases :
+ * Use of record claim for all records, in simulation only:
+ python -W ignore manage.py migrate_bbb_recordings --use-manual-claim --dry
+
+ * Use of record claim for only 2 records, in simulation only:
+ python -W ignore manage.py migrate_bbb_recordings --min-value-record-process=1
+ --max-value-record-process=2 --use-manual-claim --dry &
+
+ - Use of external video import module, with access to Moodle database for
+ all recordings, in simulation only:
+ * python -W ignore manage.py migrate_bbb_recordings --use-import-video
+ --use-database-moodle --dry
+
+Documentation for this system is available on the Esup-Pod Wiki:
+https://www.esup-portail.org/wiki/x/C4CFUQ
+
+"""
+import hashlib
+import json
+import os
+import requests
+import traceback
+from datetime import datetime as dt
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.contrib.sites.shortcuts import get_current_site
+from django.core.management.base import BaseCommand
+from django.template.defaultfilters import slugify
+from pod.import_video.utils import manage_download, parse_remote_file
+from pod.import_video.utils import manage_recording_url, check_url_exists
+from pod.import_video.models import ExternalRecording
+from pod.import_video.views import start_bbb_encode_presentation_and_move_to_destination
+from pod.meeting.models import Meeting
+from pod.recorder.models import Recorder
+from xml.dom import minidom
+
+# For PostgreSQL database #
+# Don't forget to run the following command the 1st time
+# pip install psycopg2-binary
+import psycopg2
+import psycopg2.extras
+from psycopg2 import sql
+
+# For MariaDB/MySQL database #
+# Don't forget to run the following command the 1st time
+# pip install mysql-connector-python
+# import mysql.connector
+
+
+# # Script config (TO EDIT) # #
+# Old BigBlueButton config #
+# Old BigBlueButton/Scalelite server URL
+SCRIPT_BBB_SERVER_URL = 'https://bbb.univ.fr/'
+# BigBlueButton key or Scalelite LOADBALANCER_SECRET
+SCRIPT_BBB_SECRET_KEY = ''
+# BBB or Scalelite version is greater than 2.3
+# Useful for presentation playback in 2.0 (for BBB <= 2.2) or 2.3 (for BBB >= 2.3) format
+# Set to True by default
+SCRIPT_PLAYBACK_URL_23 = True
+# #
+
+# use-manual-claim #
+# Recorder used to get BBB recordings
+SCRIPT_RECORDER_ID = 1
+# #
+
+# use-import-video #
+# Administrator to whom recordings will be added,
+# whose moderators have not been identified
+SCRIPT_ADMIN_ID = 1
+# #
+
+# use-database-moodle #
+# Moodle database connection parameters
+DB_PARAMS = {
+ 'host': 'bddmoodle.univ.fr',
+ 'database': 'moodle',
+ 'user': 'moodle',
+ 'password': '',
+ 'port': '',
+ 'connect_timeout': '10'
+}
+# Information message set in Moodle database, table mdl_bigbluebuttonbn, field intro
+SCRIPT_INFORM = (
+ ""
+ "Suite au changement d'infrastructure BigBlueButton, les enregistrements BBB "
+ "réalisées avant le 01/06/2024 ne sont plus accessibles par défaut dans Moodle. "
+ "Ces enregistrements seront disponibles du 01/06/2024 au 01/12/2024 sur Pod"
+ "("
+ "pod.univ.fr), "
+ "via le module Importer une vidéo externe. "
+ "Vous retrouverez dans ce module vos anciens enregistrements BBB, qu'il vous sera "
+ "possible de convertir en vidéo pour les rendre accessibles à vos usagers. "
+ "Pour plus d'informations sur cette migration, n'hésitez pas à consulter "
+ "la page dédiée sur le site numerique.univ.fr"
+ ". "
+ "Accéder au module d'import des vidéos dans Pod."
+ " "
+)
+# #
+# # # #
+
+
+# # Config from settings-local.py (CUSTOM SETTINGS) # #
+# Path used in the event of a claim
+DEFAULT_RECORDER_PATH = getattr(settings, "DEFAULT_RECORDER_PATH", "/data/ftp-pod/ftp/")
+# # # #
+
+# Global variable
+number_records_to_encode = 0
+
+
+def connect_moodle_database():
+ """Connect to the Moodle database and returns cursor."""
+ try:
+ # For Postgre database
+ connection = psycopg2.connect(**DB_PARAMS)
+ cursor = connection.cursor(cursor_factory=psycopg2.extras.DictCursor)
+ # For MariaDB/MySQL database
+ # connection = mysql.connector.connect(**DB_PARAMS)
+ # cursor = connection.cursor()
+ return connection, cursor
+
+ # For MariaDB/MySQL database
+ # except mysql.connector.Error as e:
+ # For Postgre database
+ except psycopg2.Error as e:
+ print("Error: Unable to connect to the Moodle database.")
+ print(e)
+ return None, None
+
+
+def disconnect_moodle_database(connection, cursor):
+ """Disconnect to Moodle database."""
+ try:
+ if cursor:
+ cursor.close()
+ if connection:
+ connection.close()
+ except Exception as e:
+ print("Error: Unable to disconnect to the Moodle database.")
+ print(e)
+
+
+def process(options):
+ """Achieve the BBB recordings migration"""
+
+ # Get the BBB recordings from BBB/Scalelite server API
+ recordings = get_bbb_recordings_by_xml()
+
+ print("***Total number of records: %s***" % len(recordings))
+
+ number_record_processed = (
+ options['max_value_record_process'] - options['min_value_record_process'] + 1
+ )
+ print("***Number of records to be processed: %s***" % number_record_processed)
+
+ # Manage the recordings
+ i = 0
+ for recording in recordings:
+ i += 1
+ # Only recordings within the interval are taken into account.
+ if (
+ i >= options['min_value_record_process']
+ and i <= options['max_value_record_process']
+ ):
+ # Get the recording
+ generic_recording = get_recording(recording)
+ if options['use_manual_claim']:
+ # #1 Use Manual claim
+ print(
+ "------------------------------\n"
+ "Use manual claim for recording #%s %s" % (
+ str(i),
+ generic_recording.internal_meeting_id
+ )
+ )
+ # The claim requires the encoding of records in presentation.
+ process_recording_to_claim(options, generic_recording)
+ print("------------------------------")
+ elif options['use_import_video']:
+ # #2 Use import video
+ print(
+ "------------------------------\n"
+ "Use import video for recording #%s %s" % (
+ str(i),
+ generic_recording.internal_meeting_id
+ )
+ )
+ process_recording_to_import_video(options, generic_recording)
+ print("------------------------------")
+ # Number of recordings to encode
+ print("***Number of records to encode in video: %s***" % number_records_to_encode)
+
+
+def get_bbb_recordings_by_xml():
+ """Get the BBB recordings from BBB/Scalelite server."""
+ print("\n*** Get the BBB recordings from BBB/Scalelite server. ***")
+ recordings = []
+ try:
+ # See https://docs.bigbluebutton.org/dev/api.html#usage
+ # for checksum and security
+ checksum = hashlib.sha1(
+ str("getRecordings" + SCRIPT_BBB_SECRET_KEY).encode("utf-8")
+ ).hexdigest()
+ # Request on BBB/Scalelite server (API)
+ # URL example:
+ # https://bbb.univ.fr/bigbluebutton/api/getRecordings?checksum=xxxx
+ urlToRequest = SCRIPT_BBB_SERVER_URL
+ urlToRequest += "bigbluebutton/api/getRecordings?checksum=" + checksum
+ addr = requests.get(urlToRequest)
+ print(
+ "Request on URL: " + urlToRequest + ", status: " + str(addr.status_code)
+ )
+ # XML result to parse
+ xmldoc = minidom.parseString(addr.text)
+ returncode = xmldoc.getElementsByTagName("returncode")[0].firstChild.data
+ # Management of FAILED error (basically error in checksum)
+ if returncode == "FAILED":
+ err = "Return code = FAILED for: " + urlToRequest
+ err += " => : " + xmldoc.toxml() + ""
+ print(err)
+ # Actual recordings
+ recordings = xmldoc.getElementsByTagName("recording")
+ except Exception as e:
+ err = (
+ "Problem to parse XML recordings on the BBB/Scalelite server "
+ "or save in Pod database: " + str(e) + ". " + traceback.format_exc()
+ )
+ print(err)
+ return recordings
+
+
+def get_recording(recording): # noqa: C901
+ """Returns a BBB recording, using the Generic_recording class."""
+ generic_recording = None
+ try:
+ # Get recording informations
+ internal_meeting_id = recording.getElementsByTagName("internalMeetingID")[
+ 0
+ ].firstChild.data
+
+ meeting_id = recording.getElementsByTagName("meetingID")[
+ 0
+ ].firstChild.data
+ meeting_name = recording.getElementsByTagName("name")[
+ 0
+ ].firstChild.data
+ start_time = recording.getElementsByTagName("startTime")[
+ 0
+ ].firstChild.data
+ # Origin can be empty (Pod or other clients), Greenlight, Moodle...
+ origin = ""
+ if recording.getElementsByTagName("bbb-origin"):
+ origin = recording.getElementsByTagName("bbb-origin")[
+ 0
+ ].firstChild.data
+
+ # Get recording URL that corresponds to the presentation URL
+ # Take only the "presentation" or "video" format
+ # Not other format like "screenshare" or "podcast"
+ presentation_url = ""
+ video_url = ""
+ # Check playback data
+ for playback in recording.getElementsByTagName("playback"):
+ # Depends on BBB parameters, we can have multiple format
+ for format in playback.getElementsByTagName("format"):
+ type = format.getElementsByTagName("type")[0].firstChild.data
+ # For bbb-recorder, we need URL of presentation format
+ if type == "presentation":
+ # Recording URL is the BBB presentation URL
+ presentation_url = format.getElementsByTagName("url")[
+ 0
+ ].firstChild.data
+ # Convert format 2.0 to 2.3 if necessary
+ presentation_url = convert_format(
+ presentation_url,
+ internal_meeting_id
+ )
+ if type == "video":
+ # Recording URL is the BBB video URL
+ video_url = format.getElementsByTagName("url")[0].firstChild.data
+
+ # Define the result with the Generic_recording class
+ generic_recording = Generic_recording(
+ internal_meeting_id,
+ meeting_id,
+ meeting_name,
+ start_time,
+ origin,
+ presentation_url,
+ video_url
+ )
+ except Exception as e:
+ err = "Problem to get BBB recording: " + str(e) + ". " + traceback.format_exc()
+ print(err)
+ return generic_recording
+
+
+def get_video_file_name(file_name, date, extension):
+ """Normalize a video file name"""
+ slug = slugify("%s %s" % (file_name[0:40], str(date)[0:10]))
+ return "%s.%s" % (slug, extension)
+
+
+def download_bbb_video_file(source_url, dest_file):
+ """Download a BBB video playback"""
+ session = requests.Session()
+ # Download and parse the remote HTML file (BBB specific)
+ video_file_add = parse_remote_file(session, source_url)
+ # Verify that video exists
+ source_video_url = manage_recording_url(source_url, video_file_add)
+ if check_url_exists(source_video_url):
+ # Download the video file
+ source_video_url = manage_download(
+ session,
+ source_url,
+ video_file_add,
+ dest_file
+ )
+ else:
+ print("Unable to download %s : video file doesn't exist." % source_url)
+
+
+def process_recording_to_claim(options, generic_recording):
+ """Download video playback or encode presentation playback to recorder directory.
+
+ Please note: the claim requires the encoding of records in presentation playback.
+ For a presentation playback, a process (asynchronous if CELERY_TO_ENCODE) is started.
+ Be careful : if not asynchronous, each encoding is made in a subprocess.
+ Must used in an asynchronous way.
+ """
+ recorder = Recorder.objects.get(id=SCRIPT_RECORDER_ID)
+
+ if generic_recording.video_url != "":
+ # Video playback
+ source_url = generic_recording.video_url
+ file_name = get_video_file_name(
+ generic_recording.meeting_name,
+ generic_recording.start_date,
+ "m4v"
+ )
+ # Video file in the recorder directory
+ dest_file = os.path.join(
+ DEFAULT_RECORDER_PATH,
+ recorder.directory,
+ os.path.basename(file_name),
+ )
+ print(
+ " - Recording %s, video playback %s: "
+ "download video file into %s." %
+ (
+ generic_recording.internal_meeting_id,
+ source_url,
+ dest_file
+ )
+ )
+ if not options['dry']:
+ # Download the video file
+ download_bbb_video_file(source_url, dest_file)
+ else:
+ # Increment the number of records to be encoded
+ global number_records_to_encode
+ number_records_to_encode += 1
+
+ source_url = generic_recording.presentation_url
+ file_name = get_video_file_name(
+ generic_recording.meeting_name,
+ generic_recording.start_date,
+ "webm"
+ )
+ # Video file generated in the recorder directory
+ dest_file = os.path.join(
+ DEFAULT_RECORDER_PATH,
+ recorder.directory,
+ os.path.basename(file_name),
+ )
+ print(
+ " - Recording %s - presentation playback %s: "
+ "encode presentation into %s" %
+ (
+ generic_recording.internal_meeting_id,
+ source_url,
+ dest_file
+ )
+ )
+ if not options['dry']:
+ # Send asynchronous/synchronous encode task (depends on CELERY_TO_ENCODE)
+ # to convert presentation in video and move it into the recorder directory
+ start_bbb_encode_presentation_and_move_to_destination(
+ file_name,
+ source_url,
+ dest_file
+ )
+
+
+def process_recording_to_import_video(options, generic_recording):
+ """Add the recording to the import video in Pod.
+
+ Please note: the link must remain valid for several months,
+ until the user can import it into Pod.
+ """
+ msg = ""
+ # List of owners (generic_user)
+ generic_owners = []
+ # Default owner is the administrator (if no user found)
+ owner = User.objects.get(id=SCRIPT_ADMIN_ID)
+ # Default site
+ site = get_current_site(None)
+ # Search if this recording was made with Pod
+ meeting = get_created_in_pod(generic_recording)
+ if meeting:
+ # Recording made with Pod, we found the owner and the site
+ owner = meeting.owner
+ site = meeting.site
+ msg = "BBB session made with Pod."
+ else:
+ # Search if this recording was made with Moodle (if configured)
+ if options["use_database_moodle"]:
+ generic_owners = get_created_in_moodle(generic_recording)
+ msg = "BBB session made with Moodle."
+ if generic_owners:
+ msg += " Create user in Pod if necessary."
+ # Owners found in Moodle
+ for generic_owner in generic_owners:
+ # Create if necessary the user in Pod
+ pod_owner = get_or_create_user_pod(options, generic_owner)
+ # Create the external recording for an owner, if necessary
+ manage_external_recording(options, generic_recording, site, pod_owner, msg)
+ # Update information in Moodle (field intro)
+ set_information_in_moodle(options, generic_recording)
+ else:
+ # Only 1 owner (administrator at least if not found)
+ manage_external_recording(options, generic_recording, site, owner, msg)
+
+
+def get_created_in_pod(generic_recording):
+ """Allow to know if this recording was made with Pod.
+
+ In such a case, we know the meeting (owner information)."""
+ # Check if the recording was made by Pod client
+ meeting = Meeting.objects.filter(
+ meeting_id=generic_recording.meeting_id
+ ).first()
+ return meeting
+
+
+def get_or_create_user_pod(options, generic_owner):
+ """Returns the Pod user corresponding to the generic owner.
+
+ If necessary, create this user in Pod."""
+ # Search for user in Pod database
+ user = User.objects.filter(username=generic_owner.username).first()
+ if user:
+ return user
+ else:
+ # Create user in Pod database
+ # Owner are necessary staff;
+ # if not, this will be changed at the user 1st connection.
+ if not options['dry']:
+ # Create
+ user = User.objects.create(
+ username=generic_owner.username,
+ first_name=generic_owner.firstname,
+ last_name=generic_owner.lastname,
+ email=generic_owner.email,
+ is_staff=True,
+ is_active=True
+ )
+ return user
+ else:
+ return generic_owner
+
+
+def manage_external_recording(options, generic_recording, site, owner, msg):
+ """Print and create an external recording for a BBB recording, if necessary."""
+ print(
+ " - Recording %s, playback %s, owner %s: "
+ "create an external recording if necessary. %s" %
+ (
+ generic_recording.internal_meeting_id,
+ generic_recording.source_url,
+ owner,
+ msg
+ )
+ )
+ if not options['dry']:
+ # Create the external recording for the owner
+ create_external_recording(generic_recording, site, owner)
+
+
+def create_external_recording(generic_recording, site, owner):
+ """Create an external recording for a BBB recording, if necessary."""
+ # Check if external recording already exists for this owner
+ external_recording = ExternalRecording.objects.filter(
+ source_url=generic_recording.source_url,
+ owner=owner
+ ).first()
+ if not external_recording:
+ # We need to create a new external recording
+ ExternalRecording.objects.create(
+ name=generic_recording.meeting_name,
+ start_at=generic_recording.start_date,
+ type="bigbluebutton",
+ source_url=generic_recording.source_url,
+ site=site,
+ owner=owner
+ )
+
+
+def get_created_in_moodle(generic_recording): # noqa: C901
+ """Allow to know if this recording was made with Moodle.
+
+ In such a case, we know the list of owners."""
+ # Default value
+ owners_found = []
+ try:
+ participants = ""
+
+ connection, cursor = connect_moodle_database()
+ with cursor as c:
+ select_query = sql.SQL(
+ "SELECT b.id, b.intro, b.course, b.participants FROM "
+ "public.mdl_bigbluebuttonbn_recordings r, public.mdl_bigbluebuttonbn b "
+ "WHERE r.bigbluebuttonbnid = b.id "
+ "AND r.recordingid = '%s' "
+ "AND r.status = 2" % (generic_recording.internal_meeting_id)
+ ).format(sql.Identifier('type'))
+ c.execute(select_query)
+ results = c.fetchall()
+ for res in results:
+ # course_id = res["course"]
+ # Format for participants:
+ # '[{"selectiontype":"all","selectionid":"all","role":"viewer"},
+ # {"selectiontype":"user","selectionid":"83","role":"moderator"}]'
+ participants = res["participants"]
+ if participants != "":
+ # Parse participants as a JSON string
+ parsed_data = json.loads(participants)
+ for item in parsed_data:
+ # Search for moderators
+ if (
+ item["selectiontype"] == "user"
+ and item["role"] == "moderator"
+ ):
+ user_id_moodle = item["selectionid"]
+ user_moodle = get_moodle_user(user_id_moodle)
+ if user_moodle:
+ print(" - Moderator found in Moodle: %s %s" % (
+ user_moodle.username,
+ user_moodle.email
+ ))
+ owners_found.append(user_moodle)
+
+ except Exception as e:
+ err = (
+ "Problem to find moderators for BBB recording in Moodle"
+ ": " + str(e) + ". " + traceback.format_exc()
+ )
+ print(err)
+ return owners_found
+
+
+def set_information_in_moodle(options, generic_recording):
+ """Set information about this migration in Moodle (if possible).
+
+ Update the field : public.mdl_bigbluebuttonbn.intro in Moodle
+ to store information, if possible (database user in read/write, permissions...).
+ Use SCRIPT_INFORM.
+ """
+ try:
+ connection, cursor = connect_moodle_database()
+ with cursor as c:
+ # Request for Moodle v4
+ select_query = sql.SQL(
+ "SELECT b.id, b.intro, b.course, b.participants FROM "
+ "public.mdl_bigbluebuttonbn_recordings r, public.mdl_bigbluebuttonbn b "
+ "WHERE r.bigbluebuttonbnid = b.id "
+ "AND r.recordingid = '%s' "
+ "AND r.status = 2" % (generic_recording.internal_meeting_id)
+ ).format(sql.Identifier('type'))
+ c.execute(select_query)
+ results = c.fetchall()
+ for res in results:
+ id = res["id"]
+ intro = res["intro"]
+ # course_id = res["course"]
+ print(" - Set information in Moodle.")
+ # If SCRIPT_INFORM wasn't already added
+ if not options['dry'] and intro.find("Pod") == -1:
+ # Update
+ update_query = sql.SQL(
+ "UPDATE public.mdl_bigbluebuttonbn SET {} = %s WHERE id = %s"
+ ).format(sql.Identifier('intro'))
+ # New value for the intro column
+ new_intro = "%s %s" % (intro, SCRIPT_INFORM)
+ # Execute the UPDATE query
+ cursor.execute(update_query, (new_intro, id))
+ # Commit the transaction
+ connection.commit()
+
+ except Exception as e:
+ # Non-blocking error
+ # Typically if user not allowed to write in Moodle database or permission not set
+ err = (
+ "Not allow to set information in Moodle"
+ ": " + str(e) + ". " + traceback.format_exc()
+ )
+ print(err)
+
+
+def get_moodle_user(user_id):
+ """Returns a generic user by user id in Moodle database."""
+ dict_user = []
+ generic_user = None
+ connection, cursor = connect_moodle_database()
+ with cursor as c:
+ # Most important field: username
+ select_query = sql.SQL(
+ "SELECT id, username, firstname, lastname, email FROM public.mdl_user "
+ "WHERE id = '%s' " % (user_id)
+ ).format(sql.Identifier('type'))
+ c.execute(select_query)
+ dict_user = c.fetchone()
+ generic_user = Generic_user(
+ dict_user["id"],
+ dict_user["username"],
+ dict_user["firstname"],
+ dict_user["lastname"],
+ dict_user["email"]
+ )
+
+ return generic_user
+
+
+def convert_format(source_url, internal_meeting_id):
+ """Convert presentation playback URL if necessary (see SCRIPT_PLAYBACK_URL_23)."""
+ try:
+ # Conversion - if necessary - from
+ # https://xxx/playback/presentation/2.0/playback.html?meetingId=ID
+ # to https://xxx/playback/presentation/2.3/ID?meetingId=ID
+ if SCRIPT_PLAYBACK_URL_23 and source_url.find("/2.0/") >= 0:
+ source_url = source_url.replace(
+ "/2.0/playback.html", "/2.3/" + internal_meeting_id
+ )
+ except Exception as e:
+ err = "Problem to convert format: " + str(e) + ". " + traceback.format_exc()
+ print(err)
+
+ return source_url
+
+
+def check_system(options): # noqa: C901
+ """Check the system (configuration, recorder, access rights). Blocking function."""
+ error = False
+ if options['use_manual_claim']:
+ # A recorder is mandatory in the event of a claim
+ recorder = Recorder.objects.filter(id=SCRIPT_RECORDER_ID).first()
+ if not recorder:
+ error = True
+ print(
+ "ERROR : No recorder found with id %s. "
+ "Please create one." % SCRIPT_RECORDER_ID
+ )
+ else:
+ claim_path = os.path.join(DEFAULT_RECORDER_PATH, recorder.directory)
+ # Check directory exist
+ if not os.path.exists(claim_path):
+ error = True
+ print(
+ "ERROR : Directory %s doesn't exist. "
+ "Please configure one." % claim_path
+ )
+ if options['use_import_video']:
+ # Administrator to whom recordings will be added,
+ # whose moderators have not been identified
+ administrator = User.objects.filter(id=SCRIPT_ADMIN_ID).first()
+ if not administrator:
+ error = True
+ print(
+ "ERROR : No administrator found with id %s. "
+ "Please create one." % SCRIPT_ADMIN_ID
+ )
+ if options['use_database_moodle']:
+ # Check connection to Moodle database
+ connection, cursor = connect_moodle_database()
+ if not cursor:
+ error = True
+ print(
+ "ERROR : Unable to connect to Moodle database. Please configure "
+ "DB_PARAMS in this file, check firewall rules and permissions."
+ )
+ else:
+ disconnect_moodle_database(connection, cursor)
+
+ if error:
+ exit()
+
+
+class Generic_user:
+ """Class for a generic user."""
+ def __init__(self, user_id, username, firstname, lastname, email):
+ """Initialize."""
+ self.id = user_id
+ self.username = username
+ self.firstname = firstname
+ self.lastname = lastname
+ self.email = email
+
+ def __str__(self):
+ """Display a generic user object as string."""
+ if self.id:
+ return "%s %s (%s)" % (self.firstname, self.lastname, self.username)
+ else:
+ return "None"
+
+
+class Generic_recording:
+ """Class for a generic recording."""
+ def __init__(
+ self,
+ internal_meeting_id,
+ meeting_id,
+ meeting_name,
+ start_time,
+ origin,
+ presentation_url,
+ video_url
+ ):
+ """Initialize."""
+ self.internal_meeting_id = internal_meeting_id
+ self.meeting_id = meeting_id
+ self.meeting_name = meeting_name
+ self.start_time = start_time
+ self.origin = origin
+ self.presentation_url = presentation_url
+ self.video_url = video_url
+ # Generated formatted date
+ self.start_date = dt.fromtimestamp(float(start_time) / 1000)
+ # Generated source URL : video playback if possible
+ self.source_url = self.video_url
+ if self.source_url == "":
+ # Else presentation playback
+ self.source_url = self.presentation_url
+
+
+class Command(BaseCommand):
+ """Migrate BBB recordings into the Pod database."""
+
+ help = "Migrate BigBlueButton recordings"
+
+ def add_arguments(self, parser):
+ """Allow arguments to be used with the command."""
+ parser.add_argument(
+ "--use-manual-claim",
+ action="store_true",
+ default=False,
+ help="Use manual claim (default=False)?"
+ )
+ parser.add_argument(
+ "--use-import-video",
+ action="store_true",
+ default=False,
+ help="Use import video module to get recordings (default=False)?"
+ )
+ parser.add_argument(
+ "--use-database-moodle",
+ action="store_true",
+ default=False,
+ help=(
+ "Use Moodle database to search for moderators (default=False)? "
+ "Only useful when --use-import-video was set to True."
+ )
+ )
+ parser.add_argument(
+ "--min-value-record-process",
+ type=int,
+ default=1,
+ help="Minimum value of records to process (default=1)."
+ )
+ parser.add_argument(
+ "--max-value-record-process",
+ type=int,
+ default=10000,
+ help="Maximum value of records to process (default=10000)."
+ )
+ parser.add_argument(
+ "--dry",
+ help="Simulates what will be achieved (default=False).",
+ action="store_true",
+ default=False,
+ )
+
+ def handle(self, *args, **options):
+ """Handle the BBB migration command call."""
+
+ if options["dry"]:
+ print("Simulation mode ('dry'). Nothing will be achieved.")
+
+ # Check configuration, recoder, rights...
+ check_system(options)
+
+ # Main function
+ process(options)
|