Skip to content

Commit 3233327

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

File tree

23 files changed

+512
-113
lines changed

23 files changed

+512
-113
lines changed

peertube.env

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@ NODE_DB_LOG=false
44

55
# Database / Postgres service configuration
66
POSTGRES_USER=postgres
7+
# ggignore-start
8+
# gitguardian:ignore
79
POSTGRES_PASSWORD=postgres
10+
11+
# Generate one using `openssl rand -hex 32`
12+
PEERTUBE_SECRET=804061c0547350babbc79de045861dc90fe783b8cf9a5ae02b4d5a46fc60f78c
13+
# ggignore-end
14+
815
# Postgres database name "peertube"
916
POSTGRES_DB=peertube_dev
1017
# The database name used by PeerTube will be PEERTUBE_DB_NAME (only if set) *OR* 'peertube'+PEERTUBE_DB_SUFFIX
@@ -27,8 +34,7 @@ PEERTUBE_WEBSERVER_HTTPS=false
2734
# pass them as a comma separated array:
2835
PEERTUBE_TRUST_PROXY=["127.0.0.1", "loopback", "172.18.0.0/16"]
2936

30-
# Generate one using `openssl rand -hex 32`
31-
PEERTUBE_SECRET=804061c0547350babbc79de045861dc90fe783b8cf9a5ae02b4d5a46fc60f78c
37+
3238

3339
# E-mail configuration
3440
# If you use a Custom SMTP server

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: 90 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,84 @@ class ExternalVideo(Video):
4351
_("Video identifier"),
4452
max_length=255,
4553
help_text=_("Video identifier URL"),
54+
unique=True,
55+
)
56+
thumbnail = models.CharField(
57+
_("Thumbnails"),
58+
max_length=255,
59+
blank=True,
60+
null=True,
4661
)
62+
viewcount = models.IntegerField(_("Number of view"), default=0)
63+
videos = models.JSONField(
64+
verbose_name=_("Mp4 resolutions list"),
65+
)
66+
67+
def save(self, *args, **kwargs) -> None:
68+
"""Store an external video object in db."""
69+
newid = -1
70+
71+
# In case of creating new Video
72+
if not self.id:
73+
# previous_video_state = None
74+
try:
75+
newid = get_nextautoincrement(ExternalVideo)
76+
except Exception:
77+
try:
78+
newid = ExternalVideo.objects.latest("id").id
79+
newid += 1
80+
except Exception:
81+
newid = 1
82+
else:
83+
# previous_video_state = Video.objects.get(id=self.id)
84+
newid = self.id
85+
newid = "%04d" % newid
86+
self.slug = "%s-%s" % (newid, slugify(self.title))
87+
self.is_external = True
88+
super(ExternalVideo, self).save(*args, **kwargs)
89+
90+
@property
91+
def get_thumbnail_admin(self):
92+
return format_html(
93+
'<img style="max-width:100px" '
94+
'src="%s" alt="%s" loading="lazy">'
95+
% (
96+
self.thumbnail,
97+
self.title,
98+
)
99+
)
100+
101+
def get_thumbnail_card(self) -> str:
102+
"""Return thumbnail image card of current external video."""
103+
return (
104+
'<img class="pod-thumbnail" src="%s" alt="%s"\
105+
loading="lazy">'
106+
% (self.thumbnail, self.title)
107+
)
108+
109+
get_thumbnail_admin.fget.short_description = _("Thumbnails")
110+
111+
def get_absolute_url(self) -> str:
112+
"""Get the external video absolute URL."""
113+
return reverse("activitypub:external_video", args=[str(self.slug)])
114+
115+
def get_marker_time_for_user(video, user): # TODO: Check usage
116+
return 0
117+
118+
def get_video_mp4_json(self) -> list:
119+
"""Get the JSON representation of the MP4 video."""
120+
return [
121+
{
122+
"type": video["type"],
123+
"src": video["src"],
124+
"size": video["size"],
125+
"width": video["width"],
126+
"height": video["height"],
127+
"extension": f".{video['src'].split('.')[-1]}",
128+
"label": f"{video['height']}p",
129+
} for video in self.videos
130+
]
131+
return [{'type': 'video/mp4', 'src': f'{self.video}', 'size': 76776, 'height': 360, 'extension': '.mp4', 'label': '360p'}]
132+
list_mp4 = self.get_video_json(extensions="mp4")
133+
logger.error(f"COUCOU {list_mp4['mp4']}")
134+
return list_mp4["mp4"] if list_mp4.get("mp4") else []

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: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,59 @@
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 = [{"type": link["mediaType"], "src": link["href"], "size": link["size"], "width": link["width"], "height": link["height"]} 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 = [
24+
{"type": link["mediaType"], "src": link["href"], "size": link["size"], "width": link["width"], "height": link["height"]} for link in tags if "mediaType" in link and link["mediaType"] == "video/mp4"
25+
]
26+
27+
external_video_attributes = {
28+
"ap_id": payload["id"],
29+
"video": video_source_links[0]["src"],
30+
"videos": video_source_links,
31+
"title": payload["name"],
32+
"date_added": payload["published"],
33+
"thumbnail": [icon for icon in payload["icon"] if "thumbnails" in icon["url"]][0]["url"],
34+
"duration": int(payload["duration"].lstrip("PT").rstrip("S")),
35+
"viewcount": payload["views"],
36+
"source_instance": source_instance,
37+
}
38+
39+
if (
40+
"language" in payload
41+
and "identifier" in payload["language"]
42+
and (identifier := payload["language"]["identifier"])
43+
and identifier in LANG_CHOICES
44+
):
45+
external_video_attributes["main_lang"] = identifier
46+
47+
if "content" in payload and (content := payload["content"]):
48+
external_video_attributes["description"] = content
49+
50+
external_video, created = ExternalVideo.objects.update_or_create(
51+
ap_id=external_video_attributes["ap_id"],
52+
defaults=external_video_attributes,
53+
)
54+
55+
if created:
56+
logger.info("ActivityPub external video %s created from %s instance", external_video, source_instance)
57+
else:
58+
logger.info("ActivityPub external video %s updated from %s instance", external_video, source_instance)
59+
60+
return external_video
1361

1462

1563
def video_to_ap_video(video):
@@ -125,7 +173,6 @@ def video_urls(video):
125173
magnets may become fully optional someday
126174
https://framacolibri.org/t/comments-and-suggestions-on-the-peertube-activitypub-implementation/21215/2
127175
"""
128-
129176
return {
130177
"url": (
131178
[
@@ -141,7 +188,11 @@ def video_urls(video):
141188
{
142189
"type": "Link",
143190
"mediaType": mp4.encoding_format,
144-
"href": ap_url(mp4.source_file.url),
191+
# "href": ap_url(mp4.source_file.url),
192+
"href": ap_url(reverse(
193+
"video:video_mp4",
194+
kwargs={"id": video.id, "mp4_id": mp4.id},
195+
)),
145196
"height": mp4.height,
146197
"width": mp4.width,
147198
"size": mp4.source_file.size,
@@ -370,16 +421,16 @@ def video_icon(video):
370421
# only image/jpeg is supported on peertube
371422
# https://github.com/Chocobozzz/PeerTube/blob/b824480af7054a5a49ddb1788c26c769c89ccc8a/server/core/helpers/custom-validators/activitypub/videos.ts#L192
372423
"""
373-
if not video.thumbnail:
374-
return {}
424+
# if not video.thumbnail:
425+
# return {}
375426

376427
return {
377428
"icon": [
378429
{
379430
"type": "Image",
380-
"url": video.get_thumbnail_url(scheme=True),
381-
"width": video.thumbnail.file.width,
382-
"height": video.thumbnail.file.height,
431+
"url": video.get_thumbnail_url(scheme=True, is_activity_pub=True),
432+
"width": video.thumbnail.file.width if video.thumbnail else 640,
433+
"height": video.thumbnail.file.height if video.thumbnail else 360,
383434
# TODO: use the real media type when peertub supports JPEG
384435
# "mediaType": video.thumbnail.file_type,
385436
"mediaType": "image/jpeg",

0 commit comments

Comments
 (0)