Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
Menu
Open sidebar
jovuit
funkwhale
Commits
4e44e4e4
Commit
4e44e4e4
authored
Apr 11, 2019
by
Eliot Berriot
Browse files
Attribute artist
parent
8687a648
Changes
31
Hide whitespace changes
Inline
Side-by-side
api/funkwhale_api/common/mutations.py
View file @
4e44e4e4
...
...
@@ -2,7 +2,7 @@ import persisting_theory
from
rest_framework
import
serializers
from
django.db
import
models
from
django.db
import
models
,
transaction
class
ConfNotFound
(
KeyError
):
...
...
@@ -23,6 +23,7 @@ class Registry(persisting_theory.Registry):
return
decorator
@
transaction
.
atomic
def
apply
(
self
,
type
,
obj
,
payload
):
conf
=
self
.
get_conf
(
type
,
obj
)
serializer
=
conf
[
"serializer_class"
](
obj
,
data
=
payload
)
...
...
@@ -73,6 +74,9 @@ class MutationSerializer(serializers.Serializer):
def
apply
(
self
,
obj
,
validated_data
):
raise
NotImplementedError
()
def
post_apply
(
self
,
obj
,
validated_data
):
pass
def
get_previous_state
(
self
,
obj
,
validated_data
):
return
...
...
@@ -88,8 +92,11 @@ class UpdateMutationSerializer(serializers.ModelSerializer, MutationSerializer):
kwargs
.
setdefault
(
"partial"
,
True
)
super
().
__init__
(
*
args
,
**
kwargs
)
@
transaction
.
atomic
def
apply
(
self
,
obj
,
validated_data
):
return
self
.
update
(
obj
,
validated_data
)
r
=
self
.
update
(
obj
,
validated_data
)
self
.
post_apply
(
r
,
validated_data
)
return
r
def
validate
(
self
,
validated_data
):
if
not
validated_data
:
...
...
api/funkwhale_api/common/utils.py
View file @
4e44e4e4
...
...
@@ -201,3 +201,30 @@ def concat_dicts(*dicts):
n
.
update
(
d
)
return
n
def
get_updated_fields
(
conf
,
data
,
obj
):
"""
Given a list of fields, a dict and an object, will return the dict keys/values
that differ from the corresponding fields on the object.
"""
final_conf
=
[]
for
c
in
conf
:
if
isinstance
(
c
,
str
):
final_conf
.
append
((
c
,
c
))
else
:
final_conf
.
append
(
c
)
final_data
=
{}
for
data_field
,
obj_field
in
final_conf
:
try
:
data_value
=
data
[
data_field
]
except
KeyError
:
continue
obj_value
=
getattr
(
obj
,
obj_field
)
if
obj_value
!=
data_value
:
final_data
[
obj_field
]
=
data_value
return
final_data
api/funkwhale_api/factories.py
View file @
4e44e4e4
...
...
@@ -2,6 +2,8 @@ import uuid
import
factory
import
persisting_theory
from
django.conf
import
settings
from
faker.providers
import
internet
as
internet_provider
...
...
@@ -50,11 +52,11 @@ class FunkwhaleProvider(internet_provider.Provider):
not random enough
"""
def
federation_url
(
self
,
prefix
=
""
):
def
federation_url
(
self
,
prefix
=
""
,
local
=
False
):
def
path_generator
():
return
"{}/{}"
.
format
(
prefix
,
uuid
.
uuid4
())
domain
=
self
.
domain_name
()
domain
=
settings
.
FEDERATION_HOSTNAME
if
local
else
self
.
domain_name
()
protocol
=
"https"
path
=
path_generator
()
return
"{}://{}/{}"
.
format
(
protocol
,
domain
,
path
)
...
...
api/funkwhale_api/federation/activity.py
View file @
4e44e4e4
...
...
@@ -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
...
...
api/funkwhale_api/federation/factories.py
View file @
4e44e4e4
...
...
@@ -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
):
...
...
api/funkwhale_api/federation/jsonld.py
View file @
4e44e4e4
...
...
@@ -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
):
...
...
api/funkwhale_api/federation/models.py
View file @
4e44e4e4
...
...
@@ -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
):
"""
...
...
api/funkwhale_api/federation/routes.py
View file @
4e44e4e4
...
...
@@ -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"
}],
),
}
api/funkwhale_api/federation/serializers.py
View file @
4e44e4e4
...
...
@@ -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
])
...
...
api/funkwhale_api/music/factories.py
View file @
4e44e4e4
...
...
@@ -64,6 +64,12 @@ class ArtistFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class
Meta
:
model
=
"music.Artist"
class
Params
:
attributed
=
factory
.
Trait
(
attributed_to
=
factory
.
SubFactory
(
federation_factories
.
ActorFactory
)
)
local
=
factory
.
Trait
(
fid
=
factory
.
Faker
(
"federation_url"
,
local
=
True
))
@
registry
.
register
class
AlbumFactory
(
NoUpdateOnCreate
,
factory
.
django
.
DjangoModelFactory
):
...
...
@@ -79,6 +85,15 @@ class AlbumFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class
Meta
:
model
=
"music.Album"
class
Params
:
attributed
=
factory
.
Trait
(
attributed_to
=
factory
.
SubFactory
(
federation_factories
.
ActorFactory
)
)
local
=
factory
.
Trait
(
fid
=
factory
.
Faker
(
"federation_url"
,
local
=
True
),
artist__local
=
True
)
@
registry
.
register
class
TrackFactory
(
NoUpdateOnCreate
,
factory
.
django
.
DjangoModelFactory
):
...
...
@@ -94,6 +109,15 @@ class TrackFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
class
Meta
:
model
=
"music.Track"
class
Params
:
attributed
=
factory
.
Trait
(
attributed_to
=
factory
.
SubFactory
(
federation_factories
.
ActorFactory
)
)
local
=
factory
.
Trait
(
fid
=
factory
.
Faker
(
"federation_url"
,
local
=
True
),
album__local
=
True
)
@
factory
.
post_generation
def
license
(
self
,
created
,
extracted
,
**
kwargs
):
if
not
created
:
...
...
api/funkwhale_api/music/migrations/0038_attributed_to.py
0 → 100644
View file @
4e44e4e4
# Generated by Django 2.1.7 on 2019-04-09 09:33
from
django.db
import
migrations
,
models
import
django.db.models.deletion
class
Migration
(
migrations
.
Migration
):
dependencies
=
[
(
"federation"
,
"0017_auto_20190130_0926"
),
(
"music"
,
"0037_auto_20190103_1757"
),
]
operations
=
[
migrations
.
AddField
(
model_name
=
"artist"
,
name
=
"attributed_to"
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
"attributed_artists"
,
to
=
"federation.Actor"
,
),
),
migrations
.
AddField
(
model_name
=
"album"
,
name
=
"attributed_to"
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
"attributed_albums"
,
to
=
"federation.Actor"
,
),
),
migrations
.
AddField
(
model_name
=
"track"
,
name
=
"attributed_to"
,
field
=
models
.
ForeignKey
(
blank
=
True
,
null
=
True
,
on_delete
=
django
.
db
.
models
.
deletion
.
SET_NULL
,
related_name
=
"attributed_tracks"
,
to
=
"federation.Actor"
,
),
),
]
api/funkwhale_api/music/models.py
View file @
4e44e4e4
...
...
@@ -114,6 +114,16 @@ class APIModelMixin(models.Model):
return
super
().
save
(
**
kwargs
)
@
property
def
is_local
(
self
):
if
not
self
.
fid
:
return
True
d
=
settings
.
FEDERATION_HOSTNAME
return
self
.
fid
.
startswith
(
"http://{}/"
.
format
(
d
))
or
self
.
fid
.
startswith
(
"https://{}/"
.
format
(
d
)
)
class
License
(
models
.
Model
):
code
=
models
.
CharField
(
primary_key
=
True
,
max_length
=
100
)
...
...
@@ -178,6 +188,16 @@ class Artist(APIModelMixin):
"mbid"
:
{
"musicbrainz_field_name"
:
"id"
},
"name"
:
{
"musicbrainz_field_name"
:
"name"
},
}
# Music entities are attributed to actors, to validate that updates occur
# from an authorized account. On top of that, we consider the instance actor
# can update anything under it's own domain
attributed_to
=
models
.
ForeignKey
(
"federation.Actor"
,
null
=
True
,
blank
=
True
,