Skip to content

Commit

Permalink
[DONE] Feature import video from BBB platform with token (#1065)
Browse files Browse the repository at this point in the history
* Add search_internal_recording function

* Manage the links of recordings (video and presentation playback) to take into account the single-use token

* Add recording_with_token route

* Add functions to manage recordings used by an infrastructure that require a token

* Add the possibility to upload a recording from BBB ESR infrastructure

* Translations added for this new feature

* Manage translations in search_internal_recording function

* Manage translations in some functions

* Translations added for this new feature
  • Loading branch information
LoicBonavent authored Mar 1, 2024
1 parent 043f63f commit 4e98db4
Show file tree
Hide file tree
Showing 10 changed files with 406 additions and 75 deletions.
54 changes: 54 additions & 0 deletions pod/import_video/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
"""Models for the Import_video module."""
import requests

from urllib.parse import urlencode
import xml.etree.ElementTree as et

from django.conf import settings
from django.contrib.sites.models import Site
Expand All @@ -9,7 +13,15 @@
from django.utils import timezone
from django.utils.translation import ugettext_lazy as _

from pod.meeting.utils import (
api_call,
parseXmlToJson,
slash_join,
)

SITE_ID = getattr(settings, "SITE_ID", 1)
USE_MEETING = getattr(settings, "USE_MEETING", False)
BBB_API_URL = getattr(settings, "BBB_API_URL", "")


class ExternalRecording(models.Model):
Expand Down Expand Up @@ -121,6 +133,48 @@ class ExternalRecording(models.Model):
help_text=_("User who uploaded to Pod the video file"),
)

def search_internal_recording(self, recording_id):
"""Search for an internal recording that corresponds to recording_id.
This function checks whether an external recording has been made on the BBB
environment used by internal recordings and the meetings module.
Typically, this function recovers the single-use token of a BBB session
performed on the ESR infrastructure.
"""
if USE_MEETING:
action = "getRecordings"
parameters = {}
parameters["recordID"] = recording_id
query = urlencode(parameters)
hashed = api_call(query, action)
if BBB_API_URL == "":
msg = {}
msg["error"] = _("Unable to call BBB server.")
msg["message"] = _("Parameter %s needs to be defined.") % "BBB_API_URL"
raise ValueError(msg)
url = slash_join(BBB_API_URL, action, "?%s" % hashed)
response = requests.get(url)
if response.status_code != 200:
msg = {}
msg["error"] = _("Unable to call BBB server.")
msg["returncode"] = response.status_code
msg["message"] = response.content.decode("utf-8")
raise ValueError(msg)
result = response.content.decode("utf-8")
xmldoc = et.fromstring(result)
recording_json = parseXmlToJson(xmldoc)
if recording_json.get("returncode", "") != "SUCCESS":
msg = {}
msg["error"] = _("Unable to get recording!")
msg["returncode"] = recording_json.get("returncode", "")
msg["messageKey"] = recording_json.get("messageKey", "")
msg["message"] = recording_json.get("message", "")
raise ValueError(msg)
else:
return recording_json
else:
return ValueError(_("Method not allowed"))

def __unicode__(self):
return "%s - %s" % (self.id, self.name)

Expand Down
63 changes: 57 additions & 6 deletions pod/import_video/templates/import_video/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,28 @@
<td>{{ record.type }}</td>
<td>
{% if record.presentationUrl != "" %}
<a class="btn btn-primary pod-btn-primary btn-sm" href="{{ record.presentationUrl }}" target="_blank" title="{% trans 'Display the recording in presentation format' %}" data-bs-toggle="tooltip" data-bs-placement="top">
<i class="bi bi-file-earmark-play" aria-hidden="true"></i>
</a>
{% if record.recordingId == "" %}
<a class="btn btn-primary pod-btn-primary btn-sm" href="{{ record.presentationUrl }}" target="_blank" title="{% trans 'Display the recording in presentation format' %}" data-bs-toggle="tooltip" data-bs-placement="top">
<i class="bi bi-file-earmark-play" aria-hidden="true"></i>
</a>
{% else %}
<a id="{{ record.id }}-presentation" class="btn btn-primary pod-btn-primary btn-sm" href="javascript:void(0);" onclick="manageTokenLink('presentation', this);" title="{% trans 'Display the recording in presentation format' %}" data-bs-toggle="tooltip" data-bs-placement="top"
data-pod-recording-id="{{ record.id }}" data-pod-recording-presentation-url="{{ record.presentationUrl }}">
<i class="bi bi-file-earmark-play" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}
{% 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>
{% if record.recordingId == "" %}
<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>
{% else %}
<a id="{{ record.id }}-video" class="btn btn-primary pod-btn-primary btn-sm" href="javascript:void(0);" onclick="manageTokenLink('video', this);" title="{% trans 'Display the recording in video format' %}" data-bs-toggle="tooltip" data-bs-placement="top"
data-pod-recording-id="{{ record.id }}" data-pod-recording-video-url="{{ record.videoUrl }}">
<i class="bi bi-camera-video" aria-hidden="true"></i>
</a>
{% endif %}
{% endif %}
</td>
<td>
Expand Down Expand Up @@ -144,5 +158,42 @@
}
return answer;
}

/**
* Function that allow to manage the link of recordings (video and presentation playback) to take into account the single-use token.
**/
function manageTokenLink(type, element) {
// Get important information from element
let recordId = element.dataset.podRecordingId;
// AJAX Pod server to re-request the BBB server
urlGetRecording = "/import_video/recording_with_token/" + recordId + "/";

// Get infos from the recording (new token appears here)
fetch(urlGetRecording, {
method: "GET",
headers: {
"X-CSRFToken": "{{ csrf_token }}",
"X-Requested-With": "XMLHttpRequest",
},
dataType: "html",
cache: "no-store",
})
.then((response) => response.text())
.then((data) => {
// Get new playback URL
let jsonData = JSON.parse(data);
var urlNew;
if (type == "presentation") {
urlNew = jsonData.presentationUrl;
} else {
urlNew = jsonData.videoUrl;
}
// Open the good URL in a new tab
window.open(urlNew, '_blank');
})
.catch((error) => {
console.debug(error);
});
}
</script>
{% endblock more_script %}
5 changes: 5 additions & 0 deletions pod/import_video/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,9 @@
views.delete_external_recording,
name="delete_external_recording",
),
path(
"recording_with_token/<slug:id>/",
views.recording_with_token,
name="recording_with_token",
),
]
122 changes: 110 additions & 12 deletions pod/import_video/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils.html import mark_safe
from django.utils.translation import gettext_lazy as _
from html.parser import HTMLParser
from pod.meeting.utils import slash_join
from pod.video.models import Video
from pod.video.models import Type
from urllib.parse import parse_qs, urlparse
Expand Down Expand Up @@ -48,6 +49,11 @@

VIDEOS_DIR = getattr(settings, "VIDEOS_DIR", "videos")

# The meeting application is activated?
USE_MEETING = getattr(settings, "USE_MEETING", False)
# BBB server API
BBB_API_URL = getattr(settings, "BBB_API_URL", "")


def secure_request_for_upload(request):
"""Check that the request is correct for uploading a recording.
Expand Down Expand Up @@ -85,13 +91,8 @@ def parse_remote_file(session: Session, source_html_url: str):
try:
response = session.get(source_html_url)
if response.status_code != 200:
msg = {}
msg["error"] = _(
"The HTML file for this recording was not found on the server."
)
# If we want to display the 404/500... page to the user
# msg["message"] = response.content.decode("utf-8")
msg["message"] = _("Error number: %s") % response.status_code
# No more informations needed for common error
msg = ''
raise ValueError(msg)

# Parse the BBB video HTML file
Expand Down Expand Up @@ -160,7 +161,7 @@ def manage_recording_url(source_url: str, video_file_add: str) -> str:
if url.query:
query = parse_qs(url.query, keep_blank_values=True)
if query["token"][0]:
# 1st case (ex: ESR URL), URL likes (ex for ESR URL:)
# 1st case (ex: ESR URL), URL likes
# https://_site_/recording/_recording_id/video?token=_token_
# Get recording unique identifier
recording_id = url.path.split("/")[2]
Expand Down Expand Up @@ -236,7 +237,7 @@ def download_video_file(session: Session, source_video_url: str, dest_file: str)
# print(session.cookies.get_dict())
if response.status_code != 200:
raise ValueError(
_("The video file for this recording " "was not found on the server.")
_("The video file for this recording was not found on the server.")
)

with open(dest_file, "wb+") as file:
Expand Down Expand Up @@ -361,7 +362,8 @@ def check_source_url(source_url: str): # noqa: C901
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
- BBB ESR infrastructure : rewrite source URL if required
- Old BigBlueButton presentation : source URL for old BBB presentation playback
"""
base_url = ""
media_id = ""
Expand Down Expand Up @@ -428,6 +430,27 @@ def check_source_url(source_url: str): # noqa: C901
media_id,
)
platform = "Mediacad"
elif source_url.find("bbb.numerique-esr.fr/video/") != -1:
# BBB ESR video link
# https://univ.scalelite.bbb.numerique-esr.fr/video/#id#/
source_video_url = source_url
format = "m4v"
platform = "BBB_ESR"
elif source_url.find("bbb.numerique-esr.fr/playback/presentation/2.3/") != -1:
# BBB ESR presentation link : rewrite for video source URL
# https://univ.scalelite.bbb.numerique-esr.fr/playback/presentation/2.3/#id#
source_video_url = source_url.replace(
"/playback/presentation/2.3/", "/video/"
) + "/"
format = "m4v"
platform = "BBB_ESR"
elif source_url.find("bbb.numerique-esr.fr/recording/") != -1:
# BBB ESR video or presentation link : rewrite for video source URL if pres
# https://univ.scalelite.bbb.numerique-esr.fr/recording/#id#/presentation?
# https://univ.scalelite.bbb.numerique-esr.fr/recording/#id#/video?
source_video_url = source_url.replace("/presentation", "/video")
format = "m4v"
platform = "BBB_ESR"
elif source_url.find("/playback/presentation/2.0/playback.html?") != -1:
# Old BBB 2.x (<2.3) presentation link
# Conversion from
Expand All @@ -454,8 +477,8 @@ def check_source_url(source_url: str): # 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: BBB ESR or older BBB, format presentation
if platform == "BBB_ESR" or platform == "BBB_Presentation":
platform_type = TypeSourceURL(platform, source_video_url, format, "")

return platform_type
Expand Down Expand Up @@ -507,6 +530,79 @@ def move_file(source: str, destination: str):
print(f"Error moving the file: {e}")


def check_url_format_presentation(source_url: str) -> bool:
""" Check if the URL looks like a BBB presentation."""
presentation = False
# Management for old presentation URLs with BBB or Scalelite server
if source_url.find("/playback/presentation/2.0/playback.html?") != -1:
presentation = True
# Management for standard presentation URLs with BBB or Scalelite server
elif source_url.find("/playback/presentation/2.3/") != -1:
presentation = True
return presentation


def check_url_need_token(source_url: str) -> str:
""" Check if the URL is used by an infrastructure that need a token.
Useful for generating the single-use token required to access video file.
2 conditions :
- URL was created on the same BBB infrastructure as the meeting module,
- At present, only ESR's BBB infrastructure uses single-use tokens.
If both conditions are met, then returns the recording_id.
"""
if USE_MEETING:
# Calculate base URL for BBB meeting
meeting_base_url = slash_join(BBB_API_URL.replace("bigbluebutton/api", ""))
# If the URL was created on the same BBB ESR infrastructure as the meeting module
# Ex:
# https://univ.scalelite.bbb.numerique-esr.fr/video/##id##/
# https://univ.scalelite.bbb.numerique-esr.fr/presentation/##id##/
# https://univ.scalelite.bbb.numerique-esr.fr/recording/##id##/video?xxx
# https://univ.scalelite.bbb.numerique-esr.fr/recording/##id##/presentation?xxx
# https://univ.scalelite.bbb.numerique-esr.fr/playback/presentation/2.3/##id##
if (
source_url.find(meeting_base_url) != -1
and source_url.find("bbb.numerique-esr.fr/") != -1
):
# Get recording id
array_url = source_url.split("/")
recording_id = array_url[-2]
# Specific case
if recording_id == "2.3":
recording_id = array_url[-1]
return recording_id
else:
return ""
else:
return ""


def get_playbacks_urls_with_token(recording):
""" Get presentation and video source URL, with token, for a recording.
Check if the recording was made by an infrastructure that need a token.
In such a case, request the BBB infrastructure to get playbacks with token.
"""
# Default values
presentation_url = ""
video_url = ""
# Check if the URL is used by an infrastructure that need a token
recording_id = check_url_need_token(recording.source_url)
if recording_id != "":
# Request the BBB infrastructure to get playbacks with token
recordings = recording.search_internal_recording(recording_id)
if type(recordings.get("recordings")) is dict:
for data in recordings["recordings"].values():
for playback in data["playback"]:
if data["playback"][playback]["type"] == "video":
video_url = data["playback"][playback]["url"]
if data["playback"][playback]["type"] == "presentation":
presentation_url = data["playback"][playback]["url"]
# Return the 2 usefuls playbacks
return presentation_url, video_url


class TypeSourceURL:
"""Manage external recording source URL.
Expand Down Expand Up @@ -599,6 +695,8 @@ class StatelessRecording:
presentationUrl = ""
# Video playback URL, used as the source URL for the video file
videoUrl = ""
# Recording id (BBB format), when created on the same BBB infra as meeting module
recordingId = ""

def __init__(self, id, name, state):
"""Initiliaze."""
Expand Down
Loading

0 comments on commit 4e98db4

Please sign in to comment.