Attribute artist
This commit is contained in:
parent
8687a64873
commit
4e44e4e4b6
31 changed files with 1741 additions and 46 deletions
|
|
@ -365,27 +365,6 @@ class OutboxRouter(Router):
|
|||
return activities
|
||||
|
||||
|
||||
def recursive_getattr(obj, key, permissive=False):
|
||||
"""
|
||||
Given a dictionary such as {'user': {'name': 'Bob'}} and
|
||||
a dotted string such as user.name, returns 'Bob'.
|
||||
|
||||
If the value is not present, returns None
|
||||
"""
|
||||
v = obj
|
||||
for k in key.split("."):
|
||||
try:
|
||||
v = v.get(k)
|
||||
except (TypeError, AttributeError):
|
||||
if not permissive:
|
||||
raise
|
||||
return
|
||||
if v is None:
|
||||
return
|
||||
|
||||
return v
|
||||
|
||||
|
||||
def match_route(route, payload):
|
||||
for key, value in route.items():
|
||||
payload_value = recursive_getattr(payload, key, permissive=True)
|
||||
|
|
@ -432,6 +411,27 @@ def prepare_deliveries_and_inbox_items(recipient_list, type):
|
|||
remote_inbox_urls.add(actor.shared_inbox_url or actor.inbox_url)
|
||||
urls.append(r["target"].followers_url)
|
||||
|
||||
elif isinstance(r, dict) and r["type"] == "instances_with_followers":
|
||||
# we want to broadcast the activity to other instances service actors
|
||||
# when we have at least one follower from this instance
|
||||
follows = (
|
||||
models.LibraryFollow.objects.filter(approved=True)
|
||||
.exclude(actor__domain_id=settings.FEDERATION_HOSTNAME)
|
||||
.exclude(actor__domain=None)
|
||||
.union(
|
||||
models.Follow.objects.filter(approved=True)
|
||||
.exclude(actor__domain_id=settings.FEDERATION_HOSTNAME)
|
||||
.exclude(actor__domain=None)
|
||||
)
|
||||
)
|
||||
actors = models.Actor.objects.filter(
|
||||
managed_domains__name__in=follows.values_list(
|
||||
"actor__domain_id", flat=True
|
||||
)
|
||||
)
|
||||
values = actors.values("shared_inbox_url", "inbox_url")
|
||||
for v in values:
|
||||
remote_inbox_urls.add(v["shared_inbox_url"] or v["inbox_url"])
|
||||
deliveries = [models.Delivery(inbox_url=url) for url in remote_inbox_urls]
|
||||
inbox_items = [
|
||||
models.InboxItem(actor=actor, type=type) for actor in local_recipients
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ class DomainFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
|||
model = "federation.Domain"
|
||||
django_get_or_create = ("name",)
|
||||
|
||||
@factory.post_generation
|
||||
def with_service_actor(self, create, extracted, **kwargs):
|
||||
if not create or not extracted:
|
||||
return
|
||||
|
||||
self.service_actor = ActorFactory(domain=self)
|
||||
self.save(update_fields=["service_actor"])
|
||||
return self.service_actor
|
||||
|
||||
|
||||
@registry.register
|
||||
class ActorFactory(NoUpdateOnCreate, factory.DjangoModelFactory):
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ def insert_context(ctx, doc):
|
|||
existing = doc["@context"]
|
||||
if isinstance(existing, list):
|
||||
if ctx not in existing:
|
||||
existing = existing[:]
|
||||
existing.append(ctx)
|
||||
doc["@context"] = existing
|
||||
else:
|
||||
doc["@context"] = [existing, ctx]
|
||||
return doc
|
||||
|
|
@ -215,6 +217,15 @@ def get_default_context():
|
|||
return ["https://www.w3.org/ns/activitystreams", "https://w3id.org/security/v1", {}]
|
||||
|
||||
|
||||
def get_default_context_fw():
|
||||
return [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
{},
|
||||
"https://funkwhale.audio/ns",
|
||||
]
|
||||
|
||||
|
||||
class JsonLdSerializer(serializers.Serializer):
|
||||
def run_validation(self, data=empty):
|
||||
if data and data is not empty and self.context.get("expand", True):
|
||||
|
|
|
|||
|
|
@ -264,6 +264,25 @@ class Actor(models.Model):
|
|||
self.private_key = v[0].decode("utf-8")
|
||||
self.public_key = v[1].decode("utf-8")
|
||||
|
||||
def can_manage(self, obj):
|
||||
attributed_to = getattr(obj, "attributed_to_id", None)
|
||||
if attributed_to is not None and attributed_to == self.pk:
|
||||
# easiest case, the obj is attributed to the actor
|
||||
return True
|
||||
|
||||
if self.domain.service_actor_id != self.pk:
|
||||
# actor is not system actor, so there is no way the actor can manage
|
||||
# the object
|
||||
return False
|
||||
|
||||
# actor is service actor of its domain, so if the fid domain
|
||||
# matches, we consider the actor has the permission to manage
|
||||
# the object
|
||||
domain = self.domain_id
|
||||
return obj.fid.startswith("http://{}/".format(domain)) or obj.fid.startswith(
|
||||
"https://{}/".format(domain)
|
||||
)
|
||||
|
||||
|
||||
class InboxItem(models.Model):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import logging
|
|||
from funkwhale_api.music import models as music_models
|
||||
|
||||
from . import activity
|
||||
from . import actors
|
||||
from . import serializers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -269,3 +270,79 @@ def outbox_delete_audio(context):
|
|||
serializer.data, to=[{"type": "followers", "target": library}]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def handle_library_entry_update(payload, context, queryset, serializer_class):
|
||||
actor = context["actor"]
|
||||
obj_id = payload["object"].get("id")
|
||||
if not obj_id:
|
||||
logger.debug("Discarding update of empty obj")
|
||||
return
|
||||
|
||||
try:
|
||||
obj = queryset.select_related("attributed_to").get(fid=obj_id)
|
||||
except queryset.model.DoesNotExist:
|
||||
logger.debug("Discarding update of unkwnown obj %s", obj_id)
|
||||
return
|
||||
if not actor.can_manage(obj):
|
||||
logger.debug(
|
||||
"Discarding unauthorize update of obj %s from %s", obj_id, actor.fid
|
||||
)
|
||||
return
|
||||
|
||||
serializer = serializer_class(obj, data=payload["object"])
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
else:
|
||||
logger.debug(
|
||||
"Discarding update of obj %s because of payload errors: %s",
|
||||
obj_id,
|
||||
serializer.errors,
|
||||
)
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Track"})
|
||||
def inbox_update_track(payload, context):
|
||||
return handle_library_entry_update(
|
||||
payload,
|
||||
context,
|
||||
queryset=music_models.Track.objects.all(),
|
||||
serializer_class=serializers.TrackSerializer,
|
||||
)
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Artist"})
|
||||
def inbox_update_artist(payload, context):
|
||||
return handle_library_entry_update(
|
||||
payload,
|
||||
context,
|
||||
queryset=music_models.Artist.objects.all(),
|
||||
serializer_class=serializers.ArtistSerializer,
|
||||
)
|
||||
|
||||
|
||||
@inbox.register({"type": "Update", "object.type": "Album"})
|
||||
def inbox_update_album(payload, context):
|
||||
return handle_library_entry_update(
|
||||
payload,
|
||||
context,
|
||||
queryset=music_models.Album.objects.all(),
|
||||
serializer_class=serializers.AlbumSerializer,
|
||||
)
|
||||
|
||||
|
||||
@outbox.register({"type": "Update", "object.type": "Track"})
|
||||
def outbox_update_track(context):
|
||||
track = context["track"]
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{"type": "Update", "object": serializers.TrackSerializer(track).data}
|
||||
)
|
||||
|
||||
yield {
|
||||
"type": "Update",
|
||||
"actor": actors.get_service_actor(),
|
||||
"payload": with_recipients(
|
||||
serializer.data,
|
||||
to=[activity.PUBLIC_ADDRESS, {"type": "instances_with_followers"}],
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,11 @@ from django.core.paginator import Paginator
|
|||
from rest_framework import serializers
|
||||
|
||||
from funkwhale_api.common import utils as funkwhale_utils
|
||||
from funkwhale_api.music import licenses
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.music import tasks as music_tasks
|
||||
|
||||
from . import activity, contexts, jsonld, models, utils
|
||||
from . import activity, actors, contexts, jsonld, models, utils
|
||||
|
||||
AP_CONTEXT = jsonld.get_default_context()
|
||||
|
||||
|
|
@ -670,7 +672,7 @@ class CollectionPageSerializer(jsonld.JsonLdSerializer):
|
|||
"first": jsonld.first_id(contexts.AS.first),
|
||||
"last": jsonld.first_id(contexts.AS.last),
|
||||
"next": jsonld.first_id(contexts.AS.next),
|
||||
"prev": jsonld.first_id(contexts.AS.next),
|
||||
"prev": jsonld.first_id(contexts.AS.prev),
|
||||
"partOf": jsonld.first_id(contexts.AS.partOf),
|
||||
}
|
||||
|
||||
|
|
@ -731,6 +733,7 @@ MUSIC_ENTITY_JSONLD_MAPPING = {
|
|||
"name": jsonld.first_val(contexts.AS.name),
|
||||
"published": jsonld.first_val(contexts.AS.published),
|
||||
"musicbrainzId": jsonld.first_val(contexts.FW.musicbrainzId),
|
||||
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -739,9 +742,29 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
|||
published = serializers.DateTimeField()
|
||||
musicbrainzId = serializers.UUIDField(allow_null=True, required=False)
|
||||
name = serializers.CharField(max_length=1000)
|
||||
attributedTo = serializers.URLField(max_length=500, allow_null=True, required=False)
|
||||
updateable_fields = []
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
attributed_to_fid = validated_data.get("attributedTo")
|
||||
if attributed_to_fid:
|
||||
validated_data["attributedTo"] = actors.get_actor(attributed_to_fid)
|
||||
updated_fields = funkwhale_utils.get_updated_fields(
|
||||
self.updateable_fields, validated_data, instance
|
||||
)
|
||||
if updated_fields:
|
||||
return music_tasks.update_library_entity(instance, updated_fields)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class ArtistSerializer(MusicEntitySerializer):
|
||||
updateable_fields = [
|
||||
("name", "name"),
|
||||
("musicbrainzId", "mbid"),
|
||||
("attributedTo", "attributed_to"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
jsonld_mapping = MUSIC_ENTITY_JSONLD_MAPPING
|
||||
|
||||
|
|
@ -752,6 +775,9 @@ class ArtistSerializer(MusicEntitySerializer):
|
|||
"name": instance.name,
|
||||
"published": instance.creation_date.isoformat(),
|
||||
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
|
||||
"attributedTo": instance.attributed_to.fid
|
||||
if instance.attributed_to
|
||||
else None,
|
||||
}
|
||||
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
|
|
@ -765,6 +791,12 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
cover = LinkSerializer(
|
||||
allowed_mimetypes=["image/*"], allow_null=True, required=False
|
||||
)
|
||||
updateable_fields = [
|
||||
("name", "title"),
|
||||
("musicbrainzId", "mbid"),
|
||||
("attributedTo", "attributed_to"),
|
||||
("released", "release_date"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
jsonld_mapping = funkwhale_utils.concat_dicts(
|
||||
|
|
@ -791,6 +823,9 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
instance.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"attributedTo": instance.attributed_to.fid
|
||||
if instance.attributed_to
|
||||
else None,
|
||||
}
|
||||
if instance.cover:
|
||||
d["cover"] = {
|
||||
|
|
@ -812,6 +847,16 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
license = serializers.URLField(allow_null=True, required=False)
|
||||
copyright = serializers.CharField(allow_null=True, required=False)
|
||||
|
||||
updateable_fields = [
|
||||
("name", "title"),
|
||||
("musicbrainzId", "mbid"),
|
||||
("attributedTo", "attributed_to"),
|
||||
("disc", "disc_number"),
|
||||
("position", "position"),
|
||||
("copyright", "copyright"),
|
||||
("license", "license"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
jsonld_mapping = funkwhale_utils.concat_dicts(
|
||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
|
|
@ -846,6 +891,9 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
"album": AlbumSerializer(
|
||||
instance.album, context={"include_ap_context": False}
|
||||
).data,
|
||||
"attributedTo": instance.attributed_to.fid
|
||||
if instance.attributed_to
|
||||
else None,
|
||||
}
|
||||
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
|
|
@ -855,13 +903,53 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
def create(self, validated_data):
|
||||
from funkwhale_api.music import tasks as music_tasks
|
||||
|
||||
metadata = music_tasks.federation_audio_track_to_metadata(validated_data)
|
||||
references = {}
|
||||
actors_to_fetch = set()
|
||||
actors_to_fetch.add(
|
||||
funkwhale_utils.recursive_getattr(
|
||||
validated_data, "attributedTo", permissive=True
|
||||
)
|
||||
)
|
||||
actors_to_fetch.add(
|
||||
funkwhale_utils.recursive_getattr(
|
||||
validated_data, "album.attributedTo", permissive=True
|
||||
)
|
||||
)
|
||||
artists = (
|
||||
funkwhale_utils.recursive_getattr(
|
||||
validated_data, "artists", permissive=True
|
||||
)
|
||||
or []
|
||||
)
|
||||
album_artists = (
|
||||
funkwhale_utils.recursive_getattr(
|
||||
validated_data, "album.artists", permissive=True
|
||||
)
|
||||
or []
|
||||
)
|
||||
for artist in artists + album_artists:
|
||||
actors_to_fetch.add(artist.get("attributedTo"))
|
||||
|
||||
for url in actors_to_fetch:
|
||||
if not url:
|
||||
continue
|
||||
references[url] = actors.get_actor(url)
|
||||
|
||||
metadata = music_tasks.federation_audio_track_to_metadata(
|
||||
validated_data, references
|
||||
)
|
||||
|
||||
from_activity = self.context.get("activity")
|
||||
if from_activity:
|
||||
metadata["from_activity_id"] = from_activity.pk
|
||||
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
|
||||
return track
|
||||
|
||||
def update(self, obj, validated_data):
|
||||
if validated_data.get("license"):
|
||||
validated_data["license"] = licenses.match(validated_data["license"])
|
||||
return super().update(obj, validated_data)
|
||||
|
||||
|
||||
class UploadSerializer(jsonld.JsonLdSerializer):
|
||||
type = serializers.ChoiceField(choices=[contexts.AS.Audio])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue