Skip to content

Commit 81767f2

Browse files
committed
Use ActivityPub models for external videos
1 parent bee3bbd commit 81767f2

File tree

22 files changed

+485
-111
lines changed

22 files changed

+485
-111
lines changed

pod/activitypub/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,5 @@ curl -H "Accept: application/activity+json, application/ld+json" -s "http://pod.
137137
### Unit tests
138138

139139
```shell
140-
python manage.py test --settings=pod.main.test_settings pod.activitypub.test_settings
140+
python manage.py test --settings=pod.main.test_settings pod.activitypub.tests
141141
```

pod/activitypub/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +0,0 @@
1-

pod/activitypub/admin.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from django.contrib import admin
22
from django.utils.translation import gettext_lazy as _
33

4-
from .models import Follower, Following
4+
from .models import Follower, Following, ExternalVideo
55
from .tasks import task_follow, task_index_videos
66

77

@@ -27,4 +27,33 @@ def reindex_videos(modeladmin, request, queryset):
2727
@admin.register(Following)
2828
class FollowingAdmin(admin.ModelAdmin):
2929
actions = [send_federation_request, reindex_videos]
30-
list_display = ("object", "status")
30+
list_display = (
31+
"object",
32+
"status",
33+
)
34+
35+
36+
# TODO External video admin
37+
@admin.register(ExternalVideo)
38+
class ExternalVideoAdmin(admin.ModelAdmin):
39+
list_display = (
40+
"id",
41+
"title",
42+
"source_instance",
43+
"date_added",
44+
"viewcount",
45+
"duration_in_time",
46+
"get_thumbnail_admin",
47+
)
48+
list_display_links = ("id", "title")
49+
list_filter = (
50+
"date_added",
51+
)
52+
53+
search_fields = [
54+
"id",
55+
"title",
56+
"video",
57+
"source_instance__object",
58+
]
59+
list_per_page = 20

pod/activitypub/apps.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from django.apps import AppConfig
2-
from django.db.models.signals import post_delete, post_save
2+
from django.db.models.signals import post_delete, pre_save, post_save
33

44

55
class ActivitypubConfig(AppConfig):
@@ -9,7 +9,8 @@ class ActivitypubConfig(AppConfig):
99
def ready(self):
1010
from pod.video.models import Video
1111

12-
from .signals import on_video_delete, on_video_save
12+
from .signals import on_video_delete, on_video_save, on_video_pre_save
1313

14+
pre_save.connect(on_video_pre_save, sender=Video)
1415
post_save.connect(on_video_save, sender=Video)
1516
post_delete.connect(on_video_delete, sender=Video)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from pod.activitypub.models import ExternalVideo
2+
3+
4+
def get_available_external_videos_filter(request=None):
5+
"""Return the base filter to get the available external videos of the site."""
6+
7+
return ExternalVideo.objects.filter()
8+
9+
10+
def get_available_external_videos(request=None):
11+
"""Get all external videos available."""
12+
return get_available_external_videos_filter(request).distinct()

pod/activitypub/models.py

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
from django.db import models
22
from django.utils.translation import ugettext_lazy as _
3+
from django.template.defaultfilters import slugify
4+
from django.utils.html import format_html
5+
from django.urls import reverse
36

4-
from pod.video.models import Video
7+
8+
from pod.video.models import BaseVideo
9+
from pod.main.models import get_nextautoincrement
510

611

712
class Follower(models.Model):
@@ -31,8 +36,11 @@ class Status(models.IntegerChoices):
3136
default=Status.NONE,
3237
)
3338

39+
def __str__(self) -> str:
40+
return self.object
41+
3442

35-
class ExternalVideo(Video):
43+
class ExternalVideo(BaseVideo):
3644
source_instance = models.ForeignKey(
3745
Following,
3846
on_delete=models.CASCADE,
@@ -43,4 +51,68 @@ class ExternalVideo(Video):
4351
_("Video identifier"),
4452
max_length=255,
4553
help_text=_("Video identifier URL"),
54+
unique=True,
55+
)
56+
video = models.CharField(
57+
_("Video source"),
58+
max_length=255,
59+
help_text=_("Video source URL"),
4660
)
61+
thumbnail = models.CharField(
62+
_("Thumbnails"),
63+
max_length=255,
64+
blank=True,
65+
null=True,
66+
)
67+
viewcount = models.IntegerField(_("Number of view"), default=0)
68+
69+
def save(self, *args, **kwargs) -> None:
70+
"""Store an external video object in db."""
71+
newid = -1
72+
73+
# In case of creating new Video
74+
if not self.id:
75+
# previous_video_state = None
76+
try:
77+
newid = get_nextautoincrement(ExternalVideo)
78+
except Exception:
79+
try:
80+
newid = ExternalVideo.objects.latest("id").id
81+
newid += 1
82+
except Exception:
83+
newid = 1
84+
else:
85+
# previous_video_state = Video.objects.get(id=self.id)
86+
newid = self.id
87+
newid = "%04d" % newid
88+
self.slug = "%s-%s" % (newid, slugify(self.title))
89+
self.is_external = True
90+
super(ExternalVideo, self).save(*args, **kwargs)
91+
92+
@property
93+
def get_thumbnail_admin(self):
94+
return format_html(
95+
'<img style="max-width:100px" '
96+
'src="%s" alt="%s" loading="lazy">'
97+
% (
98+
self.thumbnail,
99+
self.title,
100+
)
101+
)
102+
103+
def get_thumbnail_card(self) -> str:
104+
"""Return thumbnail image card of current external video."""
105+
return (
106+
'<img class="pod-thumbnail" src="%s" alt="%s"\
107+
loading="lazy">'
108+
% (self.thumbnail, self.title)
109+
)
110+
111+
get_thumbnail_admin.fget.short_description = _("Thumbnails")
112+
113+
def get_absolute_url(self) -> str:
114+
"""Get the external video absolute URL."""
115+
return reverse("activitypub:external_video", args=[str(self.slug)])
116+
117+
def get_marker_time_for_user(video, user): # TODO: Check usage
118+
return 0

pod/activitypub/network.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,7 @@ def index_video(following: Following, video_url):
9898
"""Read a video payload and create an ExternalVideo object"""
9999
ap_video = ap_object(video_url)
100100
logger.warning(f"TODO: Deal with video indexation {ap_video}")
101-
extvideo = ap_video_to_external_video(ap_video)
102-
extvideo.source_instance = following
103-
extvideo.save()
101+
ap_video_to_external_video(payload=ap_video, source_instance=following)
104102

105103

106104
def external_video_added_by_actor(ap_video, ap_actor):

pod/activitypub/serialization/video.py

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,56 @@
55
from pod.activitypub.constants import AP_LICENSE_MAPPING
66
from pod.activitypub.models import ExternalVideo
77
from pod.activitypub.utils import ap_url, make_magnet_url, stable_uuid
8+
from pod.video.models import LANG_CHOICES
89

10+
import logging
11+
logger = logging.getLogger(__name__)
912

10-
def ap_video_to_external_video(payload):
13+
14+
def ap_video_to_external_video(payload, source_instance):
1115
"""Create an ExternalVideo object from an AP Video payload."""
12-
return ExternalVideo.objects.create()
16+
17+
video_source_links = [link for link in payload["url"] if "mediaType" in link and link["mediaType"] == "video/mp4"]
18+
if not video_source_links:
19+
tags = []
20+
for link in payload["url"]:
21+
if "tag" in link:
22+
tags.extend(link["tag"])
23+
video_source_links = [link for link in tags if "mediaType" in link and link["mediaType"] == "video/mp4"]
24+
25+
external_video_attributes = {
26+
"ap_id": payload["id"],
27+
"video": video_source_links[0]["href"],
28+
"title": payload["name"],
29+
"date_added": payload["published"],
30+
"thumbnail": [icon for icon in payload["icon"] if "thumbnails" in icon["url"]][0]["url"],
31+
"duration": int(payload["duration"].lstrip("PT").rstrip("S")),
32+
"viewcount": payload["views"],
33+
"source_instance": source_instance,
34+
}
35+
36+
if (
37+
"language" in payload
38+
and "identifier" in payload["language"]
39+
and (identifier := payload["language"]["identifier"])
40+
and identifier in LANG_CHOICES
41+
):
42+
external_video_attributes["main_lang"] = identifier
43+
44+
if "content" in payload and (content := payload["content"]):
45+
external_video_attributes["description"] = content
46+
47+
external_video, created = ExternalVideo.objects.update_or_create(
48+
ap_id=external_video_attributes["ap_id"],
49+
defaults=external_video_attributes,
50+
)
51+
52+
if created:
53+
logger.info("ActivityPub external video %s created from %s instance", external_video, source_instance)
54+
else:
55+
logger.info("ActivityPub external video %s updated from %s instance", external_video, source_instance)
56+
57+
return external_video
1358

1459

1560
def video_to_ap_video(video):
@@ -125,7 +170,6 @@ def video_urls(video):
125170
magnets may become fully optional someday
126171
https://framacolibri.org/t/comments-and-suggestions-on-the-peertube-activitypub-implementation/21215/2
127172
"""
128-
129173
return {
130174
"url": (
131175
[
@@ -141,7 +185,11 @@ def video_urls(video):
141185
{
142186
"type": "Link",
143187
"mediaType": mp4.encoding_format,
144-
"href": ap_url(mp4.source_file.url),
188+
# "href": ap_url(mp4.source_file.url),
189+
"href": ap_url(reverse(
190+
"video:video_mp4",
191+
kwargs={"id": video.id, "mp4_id": mp4.id},
192+
)),
145193
"height": mp4.height,
146194
"width": mp4.width,
147195
"size": mp4.source_file.size,
@@ -370,16 +418,16 @@ def video_icon(video):
370418
# only image/jpeg is supported on peertube
371419
# https://github.com/Chocobozzz/PeerTube/blob/b824480af7054a5a49ddb1788c26c769c89ccc8a/server/core/helpers/custom-validators/activitypub/videos.ts#L192
372420
"""
373-
if not video.thumbnail:
374-
return {}
421+
# if not video.thumbnail:
422+
# return {}
375423

376424
return {
377425
"icon": [
378426
{
379427
"type": "Image",
380-
"url": video.get_thumbnail_url(scheme=True),
381-
"width": video.thumbnail.file.width,
382-
"height": video.thumbnail.file.height,
428+
"url": video.get_thumbnail_url(scheme=True, is_activity_pub=True),
429+
"width": video.thumbnail.file.width if video.thumbnail else 640,
430+
"height": video.thumbnail.file.height if video.thumbnail else 360,
383431
# TODO: use the real media type when peertub supports JPEG
384432
# "mediaType": video.thumbnail.file_type,
385433
"mediaType": "image/jpeg",

0 commit comments

Comments
 (0)