Skip to content

Commit b2e297c

Browse files
committed
Use ActivityPub models for external videos
1 parent 77c2eba commit b2e297c

File tree

21 files changed

+488
-93
lines changed

21 files changed

+488
-93
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/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: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pod.activitypub.utils import ap_object
1010
from pod.video.models import Video
11+
from pod.activitypub.models import ExternalVideo
1112

1213
from .constants import AP_DEFAULT_CONTEXT, AP_PT_VIDEO_CONTEXT, BASE_HEADERS
1314
from .models import Follower, Following
@@ -98,9 +99,7 @@ def index_video(following: Following, video_url):
9899
"""Read a video payload and create an ExternalVideo object"""
99100
ap_video = ap_object(video_url)
100101
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()
102+
ap_video_to_external_video(payload=ap_video, source_instance=following)
104103

105104

106105
def external_video_added_by_actor(ap_video, ap_actor):

pod/activitypub/serialization/video.py

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,61 @@
11
from django.template.defaultfilters import slugify
22
from django.urls import reverse
33
from markdownify import markdownify
4+
from django.utils.translation import get_language
45

56
from pod.activitypub.constants import AP_LICENSE_MAPPING
67
from pod.activitypub.models import ExternalVideo
78
from pod.activitypub.utils import ap_url, make_magnet_url, stable_uuid
9+
from pod.video.models import LANG_CHOICES
810

11+
import logging
12+
logger = logging.getLogger(__name__)
913

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

1460

1561
def video_to_ap_video(video):
@@ -125,8 +171,16 @@ def video_urls(video):
125171
magnets may become fully optional someday
126172
https://framacolibri.org/t/comments-and-suggestions-on-the-peertube-activitypub-implementation/21215/2
127173
"""
128-
129-
return {
174+
truc = video.get_video_mp4()
175+
# print(video)
176+
# print(dir(truc[0]))
177+
# print(truc[0].id)
178+
# print(truc[0].pk)
179+
# print(truc[0].source_file.url)
180+
# print(truc[0].source_file.file)
181+
# print(type(truc[0].source_file))
182+
# print(dir(truc[0].source_file))
183+
machin = {
130184
"url": (
131185
[
132186
# Webpage
@@ -141,7 +195,11 @@ def video_urls(video):
141195
{
142196
"type": "Link",
143197
"mediaType": mp4.encoding_format,
144-
"href": ap_url(mp4.source_file.url),
198+
# "href": ap_url(mp4.source_file.url),
199+
"href": ap_url(reverse(
200+
"video:video_mp4",
201+
kwargs={"id": video.id, "mp4_id": mp4.id},
202+
)),
145203
"height": mp4.height,
146204
"width": mp4.width,
147205
"size": mp4.source_file.size,
@@ -165,6 +223,8 @@ def video_urls(video):
165223
]
166224
)
167225
}
226+
print(machin)
227+
return machin
168228

169229

170230
def video_attributions(video):
@@ -370,16 +430,16 @@ def video_icon(video):
370430
# only image/jpeg is supported on peertube
371431
# https://github.com/Chocobozzz/PeerTube/blob/b824480af7054a5a49ddb1788c26c769c89ccc8a/server/core/helpers/custom-validators/activitypub/videos.ts#L192
372432
"""
373-
if not video.thumbnail:
374-
return {}
433+
# if not video.thumbnail:
434+
# return {}
375435

376436
return {
377437
"icon": [
378438
{
379439
"type": "Image",
380-
"url": video.get_thumbnail_url(scheme=True),
381-
"width": video.thumbnail.file.width,
382-
"height": video.thumbnail.file.height,
440+
"url": video.get_thumbnail_url(scheme=True, is_activity_pub=True),
441+
"width": video.thumbnail.file.width if video.thumbnail else 640,
442+
"height": video.thumbnail.file.height if video.thumbnail else 360,
383443
# TODO: use the real media type when peertub supports JPEG
384444
# "mediaType": video.thumbnail.file_type,
385445
"mediaType": "image/jpeg",

0 commit comments

Comments
 (0)