", views.external_video, name="external_video"),
+]
diff --git a/pod/activitypub/views.py b/pod/activitypub/views.py
index aeadb02391..d542716592 100644
--- a/pod/activitypub/views.py
+++ b/pod/activitypub/views.py
@@ -8,8 +8,11 @@
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
+from django.shortcuts import render
+from django.core.exceptions import SuspiciousOperation
from pod.video.models import Channel, Video
+from pod.activitypub.models import ExternalVideo
from .constants import (
AP_DEFAULT_CONTEXT,
@@ -38,6 +41,17 @@
AP_PAGE_SIZE = 25
+TYPE_TASK = {
+ "Follow": task_handle_inbox_follow,
+ "Accept": task_handle_inbox_accept,
+ "Reject": task_handle_inbox_reject,
+ "Announce": task_handle_inbox_announce,
+ "Update": task_handle_inbox_update,
+ "Delete": task_handle_inbox_delete,
+ "Undo": task_handle_inbox_undo,
+}
+
+
def nodeinfo(request):
"""
Nodeinfo endpoint. This is the entrypoint for ActivityPub federation.
@@ -125,27 +139,8 @@ def inbox(request, username=None):
logger.warning("inbox query: %s", json.dumps(data, indent=True))
# TODO: test HTTP signature
- if not username and data["type"] == "Follow":
- task_handle_inbox_follow.delay(username, data)
-
- elif not username and data["type"] == "Accept":
- task_handle_inbox_accept.delay(username, data)
-
- elif not username and data["type"] == "Reject":
- task_handle_inbox_reject.delay(username, data)
-
- elif not username and data["type"] == "Announce":
- task_handle_inbox_announce.delay(username, data)
-
- elif not username and data["type"] == "Update":
- task_handle_inbox_update.delay(username, data)
-
- elif not username and data["type"] == "Delete":
- task_handle_inbox_delete.delay(username, data)
-
- elif not username and data["type"] == "Undo":
- task_handle_inbox_undo.delay(username, data)
-
+ if (activitypub_task := TYPE_TASK.get(data["type"], None)):
+ activitypub_task.delay(username, data)
else:
logger.debug("Ignoring inbox action: %s", data["type"])
@@ -416,3 +411,29 @@ def chapters(request, id):
],
}
return JsonResponse(response, status=200)
+
+
+def render_external_video(request, id):
+ external_video = get_object_or_404(ExternalVideo, id=id)
+ return render(
+ request,
+ "videos/video.html",
+ {
+ "channel": None,
+ "video": external_video,
+ "theme": None,
+ "listNotes": None,
+ "owner_filter": False,
+ "playlist": None,
+ },
+ )
+
+
+def external_video(request, slug):
+ try:
+ id = int(slug[: slug.find("-")])
+ except ValueError:
+ raise SuspiciousOperation("Invalid external video id")
+
+ get_object_or_404(ExternalVideo, id=id)
+ return render_external_video(request, id)
diff --git a/pod/custom/settings_local_docker_full_test.py b/pod/custom/settings_local_docker_full_test.py
index 884d291c9e..4b6b66f4f7 100644
--- a/pod/custom/settings_local_docker_full_test.py
+++ b/pod/custom/settings_local_docker_full_test.py
@@ -79,6 +79,7 @@
}
USE_XAPI_VIDEO = False
+ACTIVITYPUB_CELERY_BROKER_URL = "redis://redis:6379/5"
XAPI_CELERY_BROKER_URL = "redis://redis.localhost:6379/6"
# for maximum console logging\n
diff --git a/pod/main/configuration.json b/pod/main/configuration.json
index 5d53cf2845..0b013602cc 100644
--- a/pod/main/configuration.json
+++ b/pod/main/configuration.json
@@ -2659,6 +2659,20 @@
"pod_version_end": "",
"pod_version_init": "3.1.0"
},
+ "DEFAULT_AP_THUMBNAIL": {
+ "default_value": "img/default_ap.png",
+ "description": {
+ "en": [
+ ""
+ ],
+ "fr": [
+ "Image par défaut envoyée comme vignette, utilisée pour communiquer via ActivityPub",
+ "Cette image doit être au format png et se situer dans le répertoire static."
+ ]
+ },
+ "pod_version_end": "",
+ "pod_version_init": "XXX"
+ },
"DEFAULT_TYPE_ID": {
"default_value": 1,
"description": {
diff --git a/pod/main/static/img/default.png b/pod/main/static/img/default.png
new file mode 100644
index 0000000000..a364a1e2df
Binary files /dev/null and b/pod/main/static/img/default.png differ
diff --git a/pod/playlist/models.py b/pod/playlist/models.py
index c86fa6863a..684e677505 100644
--- a/pod/playlist/models.py
+++ b/pod/playlist/models.py
@@ -12,6 +12,7 @@
from pod.main.models import get_nextautoincrement
from pod.video.models import Video
+from pod.activitypub.models import ExternalVideo
from pod.video.utils import sort_videos_list
@@ -175,7 +176,8 @@ class PlaylistContent(models.Model):
playlist = models.ForeignKey(
Playlist, verbose_name=_("Playlist"), on_delete=models.CASCADE
)
- video = models.ForeignKey(Video, verbose_name=_("Video"), on_delete=models.CASCADE)
+ video = models.ForeignKey(Video, verbose_name=_("Video"), on_delete=models.CASCADE, blank=True, null=True)
+ external_video = models.ForeignKey(ExternalVideo, verbose_name=_("External video"), on_delete=models.CASCADE, blank=True, null=True)
date_added = models.DateTimeField(
verbose_name=_("Addition date"), default=timezone.now, editable=False
)
@@ -204,6 +206,10 @@ def save(self, *args, **kwargs) -> None:
self.rank = last_rank + 1 if last_rank is not None else 1
except Exception:
...
+ if not self.video and not self.external_video:
+ raise ValidationError(
+ _("PlaylistContent needs a Video or an ExternalVideo to be created.")
+ )
return super().save(*args, **kwargs)
def __str__(self) -> str:
diff --git a/pod/playlist/utils.py b/pod/playlist/utils.py
index 4c6d027022..037e8c1b08 100644
--- a/pod/playlist/utils.py
+++ b/pod/playlist/utils.py
@@ -1,5 +1,7 @@
"""Esup-Pod playlist utilities."""
+from typing import Union
+
from django.contrib.auth.models import User
from django.contrib.sites.models import Site
from django.db.models.functions import Lower
@@ -8,6 +10,7 @@
from django.core.handlers.wsgi import WSGIRequest
from pod.video.models import Video
+from pod.activitypub.models import ExternalVideo
from django.conf import settings
from .apps import FAVORITE_PLAYLIST_NAME
@@ -16,7 +19,7 @@
import hashlib
-def check_video_in_playlist(playlist: Playlist, video: Video) -> bool:
+def check_video_in_playlist(playlist: Playlist, video: Union[Video, ExternalVideo]) -> bool:
"""
Verify if a video is present in a playlist.
@@ -27,7 +30,10 @@ def check_video_in_playlist(playlist: Playlist, video: Video) -> bool:
Returns:
bool: True if the video is on the playlist, False otherwise
"""
- return PlaylistContent.objects.filter(playlist=playlist, video=video).exists()
+ if isinstance(video, Video):
+ return PlaylistContent.objects.filter(playlist=playlist, video=video).exists()
+ elif isinstance(video, ExternalVideo):
+ return PlaylistContent.objects.filter(playlist=playlist, external_video=video).exists()
def user_add_video_in_playlist(playlist: Playlist, video: Video) -> str:
diff --git a/pod/video/models.py b/pod/video/models.py
index cf6ffe7a93..cbba2faf62 100644
--- a/pod/video/models.py
+++ b/pod/video/models.py
@@ -47,6 +47,7 @@
from django.db.models.functions import Concat
from os.path import splitext
+
USE_PODFILE = getattr(settings, "USE_PODFILE", False)
if USE_PODFILE:
from pod.podfile.models import CustomImageModel
@@ -146,6 +147,7 @@
),
)
DEFAULT_THUMBNAIL = getattr(settings, "DEFAULT_THUMBNAIL", "img/default.svg")
+DEFAULT_AP_THUMBNAIL = getattr(settings, "DEFAULT_AP_THUMBNAIL", "img/default.png")
SECRET_KEY = getattr(settings, "SECRET_KEY", "")
NOTES_STATUS = getattr(
@@ -676,15 +678,9 @@ def default_site_discipline(sender, instance, **kwargs) -> None:
instance.site = Site.objects.get_current()
-class Video(models.Model):
+class BaseVideo(models.Model):
"""Class describing video objects."""
- video = models.FileField(
- _("Video"),
- upload_to=get_storage_path_video,
- max_length=255,
- help_text=_("You can send an audio or video file."),
- )
title = models.CharField(
_("Title"),
max_length=250,
@@ -706,6 +702,67 @@ class Video(models.Model):
editable=False,
)
sites = models.ManyToManyField(Site)
+ description = RichTextField(
+ _("Description"),
+ config_name="complete",
+ blank=True,
+ help_text=_(
+ "In this field you can describe your content, "
+ + "add all needed related information, "
+ + "and format the result using the toolbar."
+ ),
+ )
+ date_added = models.DateTimeField(_("Date added"), default=timezone.now)
+ date_evt = models.DateField(
+ _("Date of event"), default=date.today, blank=True, null=True
+ )
+ main_lang = models.CharField(
+ _("Main language"),
+ max_length=2,
+ choices=LANG_CHOICES,
+ default=get_language(),
+ help_text=_("Select the main language used in the content."),
+ )
+ tags = TagField(
+ help_text=_(
+ "Separate tags with spaces, "
+ "enclose the tags consist of several words in quotation marks."
+ ),
+ verbose_name=_("Tags"),
+ )
+ licence = models.CharField(
+ _("Licence"), max_length=8, choices=LICENCE_CHOICES, blank=True, null=True
+ )
+ duration = models.IntegerField(_("Duration"), default=0, editable=False, blank=True)
+ is_video = models.BooleanField(_("Is Video"), default=True, editable=False)
+ is_external = models.BooleanField(_("Is External Video"), default=False)
+
+ class Meta:
+ abstract = True
+
+ def __str__(self) -> str:
+ """Display a video object as string."""
+ if self.id:
+ return "%s - %s" % ("%04d" % self.id, self.title)
+ else:
+ return "None"
+
+ @property
+ def duration_in_time(self) -> str:
+ """Get the duration of a video."""
+ return time.strftime("%H:%M:%S", time.gmtime(self.duration))
+
+ duration_in_time.fget.short_description = _("Duration")
+
+
+class Video(BaseVideo):
+ """Class describing video objects."""
+ video = models.FileField(
+ _("Video"),
+ upload_to=get_storage_path_video,
+ max_length=255,
+ help_text=_("You can send an audio or video file."),
+ )
type = models.ForeignKey(
Type,
verbose_name=_("Type"),
@@ -724,20 +781,6 @@ class Video(models.Model):
+ "that they can’t delete this video."
),
)
- description = RichTextField(
- _("Description"),
- config_name="complete",
- blank=True,
- help_text=_(
- "In this field you can describe your content, "
- + "add all needed related information, "
- + "and format the result using the toolbar."
- ),
- )
- date_added = models.DateTimeField(_("Date added"), default=timezone.now)
- date_evt = models.DateField(
- _("Date of event"), default=date.today, blank=True, null=True
- )
cursus = models.CharField(
_("University course"),
max_length=1,
@@ -745,13 +788,6 @@ class Video(models.Model):
default="0",
help_text=_("Select an university course as audience target of the content."),
)
- main_lang = models.CharField(
- _("Main language"),
- max_length=2,
- choices=LANG_CHOICES,
- default=get_language(),
- help_text=_("Select the main language used in the content."),
- )
transcript = models.CharField(
_("Transcript"),
max_length=2,
@@ -759,19 +795,9 @@ class Video(models.Model):
blank=True,
help_text=_("Select an available language to transcribe the audio."),
)
- tags = TagField(
- help_text=_(
- "Separate tags with spaces, "
- "enclose the tags consist of several words in quotation marks."
- ),
- verbose_name=_("Tags"),
- )
discipline = models.ManyToManyField(
Discipline, blank=True, verbose_name=_("Disciplines")
)
- licence = models.CharField(
- _("Licence"), max_length=8, choices=LICENCE_CHOICES, blank=True, null=True
- )
channel = models.ManyToManyField(Channel, verbose_name=_("Channels"), blank=True)
theme = models.ManyToManyField(
Theme,
@@ -830,7 +856,6 @@ class Video(models.Model):
verbose_name=_("Thumbnails"),
related_name="videos",
)
- duration = models.IntegerField(_("Duration"), default=0, editable=False, blank=True)
overview = models.ImageField(
_("Overview"),
null=True,
@@ -842,7 +867,6 @@ class Video(models.Model):
encoding_in_progress = models.BooleanField(
_("Encoding in progress"), default=False, editable=False
)
- is_video = models.BooleanField(_("Is Video"), default=True, editable=False)
date_delete = models.DateField(_("Date to delete"), default=default_date_delete)
@@ -986,7 +1010,7 @@ def get_player_height(self) -> int:
"""
return 360 if self.is_video else 244
- def get_thumbnail_url(self, scheme=False) -> str:
+ def get_thumbnail_url(self, scheme=False, is_activity_pub=False) -> str:
"""Get a thumbnail url for the video."""
request = None
if self.thumbnail and self.thumbnail.file_exist():
@@ -1002,7 +1026,7 @@ def get_thumbnail_url(self, scheme=False) -> str:
[
"//",
get_current_site(request).domain,
- static(DEFAULT_THUMBNAIL),
+ static(DEFAULT_AP_THUMBNAIL if is_activity_pub else DEFAULT_THUMBNAIL),
]
)
@@ -1051,13 +1075,6 @@ def get_thumbnail_card(self) -> str:
% (thumbnail_url, self.title)
)
- @property
- def duration_in_time(self) -> str:
- """Get the duration of a video."""
- return time.strftime("%H:%M:%S", time.gmtime(self.duration))
-
- duration_in_time.fget.short_description = _("Duration")
-
@property
def encoded(self) -> bool:
"""Get the encoded status of a video."""
diff --git a/pod/video/templates/videos/video-info.html b/pod/video/templates/videos/video-info.html
index 1e7934ffcc..d965d71756 100644
--- a/pod/video/templates/videos/video-info.html
+++ b/pod/video/templates/videos/video-info.html
@@ -21,7 +21,7 @@
{{ video.get_viewcount }}
{% endif %}
- {% if USE_PLAYLIST %}
+ {% if USE_PLAYLIST and not video.is_external %}
{% endif %}
- {% if USE_PLAYLIST and USE_FAVORITES %}
+ {% if USE_PLAYLIST and not video.is_external and USE_FAVORITES %}
{% trans 'Number of favorites' %}
{% if USE_STATS_VIEW and not video.encoding_in_progress %}
@@ -42,7 +42,7 @@
{% endif %}
{% endif %}
-{% if USE_QUIZ %}
+{% if USE_QUIZ and not video.is_external %}
{% is_quiz_accessible video as is_quiz_accessible %}
{% if is_quiz_accessible or video.owner == request.user or request.user.is_superuser or request.user in video.additional_owners.all %}
diff --git a/pod/video/templates/videos/video_opengraph.html b/pod/video/templates/videos/video_opengraph.html
index 1e056aa8dd..371ca05e22 100644
--- a/pod/video/templates/videos/video_opengraph.html
+++ b/pod/video/templates/videos/video_opengraph.html
@@ -7,7 +7,6 @@
{% endif %}
-
@@ -18,23 +17,27 @@
-
-
-
-
{% if video.is_draft == True %}
-{% endif %}
\ No newline at end of file
+{% endif %}
+
+{% if not video.is_external %}
+
+
+
+
+
+{% endif %}
diff --git a/pod/video/urls.py b/pod/video/urls.py
index ed1bb6d83b..48cdcad48c 100644
--- a/pod/video/urls.py
+++ b/pod/video/urls.py
@@ -35,6 +35,7 @@
vote_get,
vote_post,
video_edit_access_tokens,
+ video_mp4_filename,
)
@@ -211,3 +212,8 @@
name="video_private",
),
]
+
+# MP4 FILENAME FOR ACTIVITYPUB PURPOSE
+urlpatterns += [
+ url(r"^mp4/(?P[\d]+)_(?P[\d]+)\.mp4$", video_mp4_filename, name="video_mp4"),
+]
diff --git a/pod/video/views.py b/pod/video/views.py
index b477d42a9d..bc6d8971f5 100644
--- a/pod/video/views.py
+++ b/pod/video/views.py
@@ -91,6 +91,8 @@
from ..ai_enhancement.utils import enhancement_is_already_asked
+from pod.activitypub.context_processors import get_available_external_videos
+
RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY = getattr(
settings, "RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY", False
)
@@ -905,6 +907,15 @@ def get_filtered_videos_list(request, videos_list):
return videos_list.distinct()
+def get_filtered_external_videos_list(request, external_videos_list):
+ """Return filtered external videos list by get parameters."""
+ if request.GET.getlist("source_instance"):
+ external_videos_list = external_videos_list.filter(
+ Q(source_instance__object__in=request.GET.getlist("source_instance"))
+ )
+ return external_videos_list.distinct()
+
+
def get_owners_has_instances(owners: list) -> list:
"""Return the list of owners who has instances in User.objects."""
ownersInstances = []
@@ -935,10 +946,11 @@ def owner_is_searchable(user: User) -> bool:
def videos(request):
"""Render the main list of videos."""
videos_list = get_filtered_videos_list(request, get_available_videos())
+ external_video_list = get_filtered_external_videos_list(request, get_available_external_videos())
sort_field = request.GET.get("sort") if request.GET.get("sort") else "date_added"
sort_direction = request.GET.get("sort_direction")
- videos_list = sort_videos_list(videos_list, sort_field, sort_direction)
+ videos_list = list(sort_videos_list(videos_list, sort_field, sort_direction)) + list(external_video_list) # TODO: Check performance
if not sort_field:
# Get the default Video ordering
@@ -3406,6 +3418,14 @@ def get_theme_list_for_specific_channel(request: WSGIRequest, slug: str) -> Json
return JsonResponse(json.loads(channel.get_all_theme_json()), safe=False)
+def video_mp4_filename(request, mp4_id, id):
+ video = get_object_or_404(Video, id=id, sites=get_current_site(request))
+
+ mp4s = [v for v in video.get_video_mp4() if str(v.id) == mp4_id]
+ if len(mp4s) == 1:
+ return redirect(mp4s[0].source_file.url)
+
+
"""
# check access to video
# change template to fix height and breadcrumbs