Skip to content

Commit

Permalink
Use ActivityPub models for external videos
Browse files Browse the repository at this point in the history
  • Loading branch information
LoanR committed Jun 28, 2024
1 parent 77c2eba commit 2d79cd9
Show file tree
Hide file tree
Showing 23 changed files with 493 additions and 117 deletions.
2 changes: 1 addition & 1 deletion pod/activitypub/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,5 @@ curl -H "Accept: application/activity+json, application/ld+json" -s "http://pod.
### Unit tests

```shell
python manage.py test --settings=pod.main.test_settings pod.activitypub.test_settings
python manage.py test --settings=pod.main.test_settings pod.activitypub.tests
```
1 change: 0 additions & 1 deletion pod/activitypub/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@

33 changes: 31 additions & 2 deletions pod/activitypub/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from django.contrib import admin
from django.utils.translation import gettext_lazy as _

from .models import Follower, Following
from .models import Follower, Following, ExternalVideo
from .tasks import task_follow, task_index_videos


Expand All @@ -27,4 +27,33 @@ def reindex_videos(modeladmin, request, queryset):
@admin.register(Following)
class FollowingAdmin(admin.ModelAdmin):
actions = [send_federation_request, reindex_videos]
list_display = ("object", "status")
list_display = (
"object",
"status",
)


# TODO External video admin
@admin.register(ExternalVideo)
class ExternalVideoAdmin(admin.ModelAdmin):
list_display = (
"id",
"title",
"source_instance",
"date_added",
"viewcount",
"duration_in_time",
"get_thumbnail_admin",
)
list_display_links = ("id", "title")
list_filter = (
"date_added",
)

search_fields = [
"id",
"title",
"video",
"source_instance__object",
]
list_per_page = 20
5 changes: 3 additions & 2 deletions pod/activitypub/apps.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.apps import AppConfig
from django.db.models.signals import post_delete, post_save
from django.db.models.signals import post_delete, pre_save, post_save


class ActivitypubConfig(AppConfig):
Expand All @@ -9,7 +9,8 @@ class ActivitypubConfig(AppConfig):
def ready(self):
from pod.video.models import Video

from .signals import on_video_delete, on_video_save
from .signals import on_video_delete, on_video_save, on_video_pre_save

pre_save.connect(on_video_pre_save, sender=Video)
post_save.connect(on_video_save, sender=Video)
post_delete.connect(on_video_delete, sender=Video)
12 changes: 12 additions & 0 deletions pod/activitypub/context_processors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pod.activitypub.models import ExternalVideo


def get_available_external_videos_filter(request=None):
"""Return the base filter to get the available external videos of the site."""

return ExternalVideo.objects.filter()


def get_available_external_videos(request=None):
"""Get all external videos available."""
return get_available_external_videos_filter(request).distinct()
76 changes: 74 additions & 2 deletions pod/activitypub/models.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
from django.db import models
from django.utils.translation import ugettext_lazy as _
from django.template.defaultfilters import slugify
from django.utils.html import format_html
from django.urls import reverse

from pod.video.models import Video

from pod.video.models import BaseVideo
from pod.main.models import get_nextautoincrement


class Follower(models.Model):
Expand Down Expand Up @@ -31,8 +36,11 @@ class Status(models.IntegerChoices):
default=Status.NONE,
)

def __str__(self) -> str:
return self.object


class ExternalVideo(Video):
class ExternalVideo(BaseVideo):
source_instance = models.ForeignKey(
Following,
on_delete=models.CASCADE,
Expand All @@ -43,4 +51,68 @@ class ExternalVideo(Video):
_("Video identifier"),
max_length=255,
help_text=_("Video identifier URL"),
unique=True,
)
video = models.CharField(
_("Video source"),
max_length=255,
help_text=_("Video source URL"),
)
thumbnail = models.CharField(
_("Thumbnails"),
max_length=255,
blank=True,
null=True,
)
viewcount = models.IntegerField(_("Number of view"), default=0)

def save(self, *args, **kwargs) -> None:
"""Store an external video object in db."""
newid = -1

# In case of creating new Video
if not self.id:
# previous_video_state = None
try:
newid = get_nextautoincrement(ExternalVideo)
except Exception:
try:
newid = ExternalVideo.objects.latest("id").id
newid += 1
except Exception:
newid = 1
else:
# previous_video_state = Video.objects.get(id=self.id)
newid = self.id
newid = "%04d" % newid
self.slug = "%s-%s" % (newid, slugify(self.title))
self.is_external = True
super(ExternalVideo, self).save(*args, **kwargs)

@property
def get_thumbnail_admin(self):
return format_html(
'<img style="max-width:100px" '
'src="%s" alt="%s" loading="lazy">'
% (
self.thumbnail,
self.title,
)
)

def get_thumbnail_card(self) -> str:
"""Return thumbnail image card of current external video."""
return (
'<img class="pod-thumbnail" src="%s" alt="%s"\
loading="lazy">'
% (self.thumbnail, self.title)
)

get_thumbnail_admin.fget.short_description = _("Thumbnails")

def get_absolute_url(self) -> str:
"""Get the external video absolute URL."""
return reverse("activitypub:external_video", args=[str(self.slug)])

def get_marker_time_for_user(video, user): # TODO: Check usage
return 0
4 changes: 1 addition & 3 deletions pod/activitypub/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,7 @@ def index_video(following: Following, video_url):
"""Read a video payload and create an ExternalVideo object"""
ap_video = ap_object(video_url)
logger.warning(f"TODO: Deal with video indexation {ap_video}")
extvideo = ap_video_to_external_video(ap_video)
extvideo.source_instance = following
extvideo.save()
ap_video_to_external_video(payload=ap_video, source_instance=following)


def external_video_added_by_actor(ap_video, ap_actor):
Expand Down
66 changes: 57 additions & 9 deletions pod/activitypub/serialization/video.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,56 @@
from pod.activitypub.constants import AP_LICENSE_MAPPING
from pod.activitypub.models import ExternalVideo
from pod.activitypub.utils import ap_url, make_magnet_url, stable_uuid
from pod.video.models import LANG_CHOICES

import logging
logger = logging.getLogger(__name__)

def ap_video_to_external_video(payload):

def ap_video_to_external_video(payload, source_instance):
"""Create an ExternalVideo object from an AP Video payload."""
return ExternalVideo.objects.create()

video_source_links = [link for link in payload["url"] if "mediaType" in link and link["mediaType"] == "video/mp4"]
if not video_source_links:
tags = []
for link in payload["url"]:
if "tag" in link:
tags.extend(link["tag"])
video_source_links = [link for link in tags if "mediaType" in link and link["mediaType"] == "video/mp4"]

external_video_attributes = {
"ap_id": payload["id"],
"video": video_source_links[0]["href"],
"title": payload["name"],
"date_added": payload["published"],
"thumbnail": [icon for icon in payload["icon"] if "thumbnails" in icon["url"]][0]["url"],
"duration": int(payload["duration"].lstrip("PT").rstrip("S")),
"viewcount": payload["views"],
"source_instance": source_instance,
}

if (
"language" in payload
and "identifier" in payload["language"]
and (identifier := payload["language"]["identifier"])
and identifier in LANG_CHOICES
):
external_video_attributes["main_lang"] = identifier

if "content" in payload and (content := payload["content"]):
external_video_attributes["description"] = content

external_video, created = ExternalVideo.objects.update_or_create(
ap_id=external_video_attributes["ap_id"],
defaults=external_video_attributes,
)

if created:
logger.info("ActivityPub external video %s created from %s instance", external_video, source_instance)
else:
logger.info("ActivityPub external video %s updated from %s instance", external_video, source_instance)

return external_video


def video_to_ap_video(video):
Expand Down Expand Up @@ -125,7 +170,6 @@ def video_urls(video):
magnets may become fully optional someday
https://framacolibri.org/t/comments-and-suggestions-on-the-peertube-activitypub-implementation/21215/2
"""

return {
"url": (
[
Expand All @@ -141,7 +185,11 @@ def video_urls(video):
{
"type": "Link",
"mediaType": mp4.encoding_format,
"href": ap_url(mp4.source_file.url),
# "href": ap_url(mp4.source_file.url),
"href": ap_url(reverse(
"video:video_mp4",
kwargs={"id": video.id, "mp4_id": mp4.id},
)),
"height": mp4.height,
"width": mp4.width,
"size": mp4.source_file.size,
Expand Down Expand Up @@ -370,16 +418,16 @@ def video_icon(video):
# only image/jpeg is supported on peertube
# https://github.com/Chocobozzz/PeerTube/blob/b824480af7054a5a49ddb1788c26c769c89ccc8a/server/core/helpers/custom-validators/activitypub/videos.ts#L192
"""
if not video.thumbnail:
return {}
# if not video.thumbnail:
# return {}

return {
"icon": [
{
"type": "Image",
"url": video.get_thumbnail_url(scheme=True),
"width": video.thumbnail.file.width,
"height": video.thumbnail.file.height,
"url": video.get_thumbnail_url(scheme=True, is_activity_pub=True),
"width": video.thumbnail.file.width if video.thumbnail else 640,
"height": video.thumbnail.file.height if video.thumbnail else 360,
# TODO: use the real media type when peertub supports JPEG
# "mediaType": video.thumbnail.file_type,
"mediaType": "image/jpeg",
Expand Down
Loading

0 comments on commit 2d79cd9

Please sign in to comment.