Skip to content

Commit

Permalink
[DONE] Feature migrate bbb recordings (#1055)
Browse files Browse the repository at this point in the history
* Add external recording state

* Manage external recording state

* Add external recording state

* Changes the logic for presentation and video URLs

* Add some tests

* Manage source URL for old BBB presentation playbacks and optimize source code

* Implements business logic for encoding BBB web presentations into video files

* Translations added for this new feature to convert BBB web presentations into video files

* Add 3 parameters : USE_IMPORT_VIDEO_BBB_RECORDER, IMPORT_VIDEO_BBB_RECORDER_PLUGIN and IMPORT_VIDEO_BBB_RECORDER_PATH

* Task management for converting BBB web presentations into video files

* Changes the function call of the video import module

* Useful script for migrating BBB records when changing BBB infrastructure (see https://www.esup-portail.org/wiki/x/C4CFUQ).

* Modify a comment

* Add some type hints

* Remove some useless translations

* Remove some useless translations

* Add some type hints

* Add some type hints
  • Loading branch information
LoicBonavent authored Feb 16, 2024
1 parent 793d1d0 commit ac17ffe
Show file tree
Hide file tree
Showing 15 changed files with 1,540 additions and 261 deletions.
1 change: 1 addition & 0 deletions pod/import_video/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class ExternalRecordingAdmin(admin.ModelAdmin):
"start_at",
"type",
"source_url",
"state",
"owner",
)
search_fields = [
Expand Down
4 changes: 2 additions & 2 deletions pod/import_video/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
7 changes: 7 additions & 0 deletions pod/import_video/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 6 additions & 4 deletions pod/import_video/templates/import_video/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
{% for record in recordings %}
<tr class="recordings_list_item">
<td class="recording_name">{{ record.name }}</td>
<td>{{ record.state }}</td>
<td>{{ record.state | safe }}</td>
<td>{{ record.startTime }}</td>
<td>{{ record.type }}</td>
<td>
Expand All @@ -61,12 +61,13 @@
<i class="bi bi-file-earmark-play" aria-hidden="true"></i>
</a>
{% endif %}
{% if record.videoUrl != "" %}
{% if record.videoUrl != "" and record.videoUrl != record.presentationUrl %}
<a class="btn btn-primary pod-btn-primary btn-sm" href="{{ record.videoUrl }}" target="_blank" title="{% trans 'Display the recording in video format' %}" data-bs-toggle="tooltip" data-bs-placement="top">
<i class="bi bi-camera-video" aria-hidden="true"></i>
</a>
{% endif %}
</td><td>
</td>
<td>
{% if record.canUpload %}
{% trans "Please confirm you want to upload the recording to Pod" as confirmUpload %}
<form method="post" action="{% url "import_video:upload_external_recording_to_pod" record_id=record.id %}" onsubmit="return confirmUploadToPod('{{ confirmUpload|escapejs }}');" style="display: inline;">
Expand All @@ -76,7 +77,8 @@
<button type="submit" class="btn btn-secondary pod-btn-secondary btn-sm" title="{% trans 'Upload to Pod as a video' %}" data-bs-toggle="tooltip" data-bs-placement="top"><i class="bi bi-upload pod-add" aria-hidden="true"></i></button>
</form>
{% endif %}
</td><td>
</td>
<td>
{% if record.canDelete %}
{% trans "Please confirm you want to delete the external recording" as confirmDelete %}
<a href="{% url 'import_video:edit_external_recording' record.id %}" class="btn btn-primary pod-btn-primary btn-sm" title="{% trans 'Edit the external recording' %}" data-bs-toggle="tooltip" data-bs-placement="top">
Expand Down
41 changes: 41 additions & 0 deletions pod/import_video/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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!")
114 changes: 82 additions & 32 deletions pod/import_video/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Utils for Meeting and Import_video module."""
"""Esup-Pod meeting and import_video utils."""

import json
import os
Expand All @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 :
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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),
Expand All @@ -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)
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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 = ""
Expand Down Expand Up @@ -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":
Expand All @@ -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:
Expand All @@ -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:
Expand Down
Loading

0 comments on commit ac17ffe

Please sign in to comment.