Resolve "ActivityStreams compliance: duration" (#1566)

This commit is contained in:
petitminion 2025-01-20 21:26:47 +00:00
parent 2636a3dde7
commit 82a1facdb5
5 changed files with 63 additions and 18 deletions

View File

@ -1,5 +1,6 @@
import logging
import os
import re
import urllib.parse
import uuid
@ -1580,6 +1581,50 @@ class TrackSerializer(MusicEntitySerializer):
return super().update(obj, validated_data)
def duration_int_to_xml(duration):
if not duration:
return None
multipliers = {"S": 1, "M": 60, "H": 3600, "D": 86400}
ret = "P"
days, seconds = divmod(int(duration), multipliers["D"])
ret += f"{days:d}DT" if days > 0 else "T"
hours, seconds = divmod(seconds, multipliers["H"])
ret += f"{hours:d}H" if hours > 0 else ""
minutes, seconds = divmod(seconds, multipliers["M"])
ret += f"{minutes:d}M" if minutes > 0 else ""
ret += f"{seconds:d}S" if seconds > 0 or ret == "PT" else ""
return ret
class DayTimeDurationSerializer(serializers.DurationField):
multipliers = {"S": 1, "M": 60, "H": 3600, "D": 86400}
def to_internal_value(self, value):
if isinstance(value, float):
return value
parsed = re.match(
r"P([0-9]+D)?T([0-9]+H)?([0-9]+M)?([0-9]+(?:\.[0-9]+)?S)?", str(value)
)
if parsed is not None:
return int(
sum(
[
self.multipliers[s[-1]] * float("0" + s[:-1])
for s in parsed.groups()
if s is not None
]
)
)
self.fail(
"invalid", format="https://www.w3.org/TR/xmlschema11-2/#dayTimeDuration"
)
def to_representation(self, value):
duration_int_to_xml(value)
class UploadSerializer(jsonld.JsonLdSerializer):
type = serializers.ChoiceField(choices=[contexts.AS.Audio])
id = serializers.URLField(max_length=500)
@ -1589,7 +1634,7 @@ class UploadSerializer(jsonld.JsonLdSerializer):
updated = serializers.DateTimeField(required=False, allow_null=True)
bitrate = serializers.IntegerField(min_value=0)
size = serializers.IntegerField(min_value=0)
duration = serializers.IntegerField(min_value=0)
duration = DayTimeDurationSerializer(min_value=0)
track = TrackSerializer(required=True)
@ -1701,7 +1746,7 @@ class UploadSerializer(jsonld.JsonLdSerializer):
"published": instance.creation_date.isoformat(),
"bitrate": instance.bitrate,
"size": instance.size,
"duration": instance.duration,
"duration": duration_int_to_xml(instance.duration),
"url": [
{
"href": utils.full_url(instance.listen_url_no_download),
@ -1851,7 +1896,7 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
url = LinkListSerializer(keep_mediatype=["audio/*"], min_length=1)
name = serializers.CharField()
published = serializers.DateTimeField(required=False)
duration = serializers.IntegerField(min_value=0, required=False)
duration = DayTimeDurationSerializer(required=False)
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
album = serializers.URLField(max_length=500, required=False)
@ -1960,7 +2005,7 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
if upload.track.local_license:
data["license"] = upload.track.local_license["identifiers"][0]
include_if_not_none(data, upload.duration, "duration")
include_if_not_none(data, duration_int_to_xml(upload.duration), "duration")
include_if_not_none(data, upload.track.position, "position")
include_if_not_none(data, upload.track.disc_number, "disc")
include_if_not_none(data, upload.track.copyright, "copyright")

View File

@ -719,7 +719,7 @@ class Track(APIModelMixin):
@property
def listen_url(self) -> str:
# Not using reverse because this is slow
return f"/api/v1/listen/{self.uuid}/"
return f"/api/v2/listen/{self.uuid}/"
@property
def local_license(self):

View File

@ -1268,7 +1268,7 @@ def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
"name": "Ignored",
"published": published.isoformat(),
"updated": updated.isoformat(),
"duration": 43,
"duration": "PT43S",
"bitrate": 42,
"size": 66,
"url": {"href": "https://audio.file", "type": "Link", "mediaType": "audio/mp3"},
@ -1337,7 +1337,7 @@ def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
assert track_create.call_count == 1
assert upload.fid == data["id"]
assert upload.track.fid == data["track"]["id"]
assert upload.duration == data["duration"]
assert upload.duration == 43
assert upload.size == data["size"]
assert upload.bitrate == data["bitrate"]
assert upload.source == data["url"]["href"]
@ -1357,7 +1357,7 @@ def test_activity_pub_upload_serializer_from_ap_update(factories, mocker, now, r
"name": "Ignored",
"published": now.isoformat(),
"updated": now.isoformat(),
"duration": 42,
"duration": "PT42S",
"bitrate": 42,
"size": 66,
"url": {
@ -1376,7 +1376,7 @@ def test_activity_pub_upload_serializer_from_ap_update(factories, mocker, now, r
upload.refresh_from_db()
assert upload.fid == data["id"]
assert upload.duration == data["duration"]
assert upload.duration == 42
assert upload.size == data["size"]
assert upload.bitrate == data["bitrate"]
assert upload.source == data["url"]["href"]
@ -1408,7 +1408,7 @@ def test_activity_pub_audio_serializer_to_ap(factories):
"name": upload.track.full_name,
"published": upload.creation_date.isoformat(),
"updated": upload.modification_date.isoformat(),
"duration": upload.duration,
"duration": "PT43S",
"bitrate": upload.bitrate,
"size": upload.size,
"to": contexts.AS.Public,
@ -1777,7 +1777,7 @@ def test_channel_upload_serializer(factories):
"content": common_utils.render_html(content.text, content.content_type),
"to": "https://www.w3.org/ns/activitystreams#Public",
"position": upload.track.position,
"duration": upload.duration,
"duration": "PT54S",
"album": upload.track.album.fid,
"disc": upload.track.disc_number,
"copyright": upload.track.copyright,
@ -1826,7 +1826,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now, mocker):
"published": now.isoformat(),
"mediaType": "text/html",
"content": "<p>Hello</p>",
"duration": 543,
"duration": "PT543S",
"position": 4,
"disc": 2,
"album": album.fid,
@ -1875,7 +1875,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now, mocker):
assert upload.mimetype == payload["url"][1]["mediaType"]
assert upload.size == payload["url"][1]["size"]
assert upload.bitrate == payload["url"][1]["bitrate"]
assert upload.duration == payload["duration"]
assert upload.duration == 543
assert upload.track.artist_credit.all()[0].artist == channel.artist
assert upload.track.position == payload["position"]
assert upload.track.disc_number == payload["disc"]
@ -1909,7 +1909,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now, mocker):
"published": now.isoformat(),
"mediaType": "text/html",
"content": "<p>Hello</p>",
"duration": 543,
"duration": "PT543S",
"position": 4,
"disc": 2,
"album": album.fid,
@ -1959,7 +1959,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now, mocker):
assert upload.mimetype == payload["url"][1]["mediaType"]
assert upload.size == payload["url"][1]["size"]
assert upload.bitrate == payload["url"][1]["bitrate"]
assert upload.duration == payload["duration"]
assert upload.duration == 543
assert upload.track.artist_credit.all()[0].artist == channel.artist
assert upload.track.position == payload["position"]
assert upload.track.disc_number == payload["disc"]

View File

@ -0,0 +1 @@
ActivityStreams compliance: duration (#1566)

View File

@ -798,8 +798,7 @@ An `Audio` object is a custom object used to store upload information. It extend
- Integer
- The bitrate of the audio in bytes/s
* - `duration`*
- Integer
- The duration of the audio in seconds
- The duration of the audio as defined in [as](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration)
* - `library`*
- String (URI)
- The ID of the audio's containing [`Library` object](#library)
@ -826,7 +825,7 @@ An `Audio` object is a custom object used to store upload information. It extend
"name": "Krav Boca - Mortem",
"size": 8656581,
"bitrate": 320000,
"duration": 213,
"duration": "PT1312S,
"library": "https://awesome.music/federation/music/libraries/dc702491-f6ce-441b-9da0-cecbed08bcc6",
"updated": "2018-10-02T19:49:35.646372+00:00",
"published": "2018-10-02T19:49:35.646359+00:00",