kopia lustrzana https://dev.funkwhale.audio/funkwhale/funkwhale
Merge branch '1568-multi-artist-backend-parser' into 'develop'
draft : multi-artist-backend-support Closes #1568 See merge request funkwhale/funkwhale!2773
commit
702c8ab8d2
|
@ -40,11 +40,17 @@ def combined_recent(limit, **kwargs):
|
|||
def get_activity(user, limit=20):
|
||||
query = fields.privacy_level_query(user, lookup_field="user__privacy_level")
|
||||
querysets = [
|
||||
Listening.objects.filter(query).select_related(
|
||||
"track", "user", "track__artist", "track__album__artist"
|
||||
Listening.objects.filter(query).prefetch_related(
|
||||
"track",
|
||||
"user",
|
||||
"track__artist_credit__artist",
|
||||
"track__album__artist_credit__artist",
|
||||
),
|
||||
TrackFavorite.objects.filter(query).select_related(
|
||||
"track", "user", "track__artist", "track__album__artist"
|
||||
TrackFavorite.objects.filter(query).prefetch_related(
|
||||
"track",
|
||||
"user",
|
||||
"track__artist_credit__artist",
|
||||
"track__album__artist_credit__artist",
|
||||
),
|
||||
]
|
||||
records = combined_recent(limit=limit, querysets=querysets)
|
||||
|
|
|
@ -21,7 +21,11 @@ TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
|||
|
||||
class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
||||
q = fields.SearchFilter(
|
||||
search_fields=["artist__name", "actor__summary", "actor__preferred_username"]
|
||||
search_fields=[
|
||||
"artist_credit__artist__name",
|
||||
"actor__summary",
|
||||
"actor__preferred_username",
|
||||
]
|
||||
)
|
||||
tag = TAG_FILTER
|
||||
scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
|
||||
|
@ -33,7 +37,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
|||
# tuple-mapping retains order
|
||||
fields=(
|
||||
("creation_date", "creation_date"),
|
||||
("artist__modification_date", "modification_date"),
|
||||
("artist_credit__artist__modification_date", "modification_date"),
|
||||
("?", "random"),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -27,6 +27,8 @@ from funkwhale_api.federation import utils as federation_utils
|
|||
from funkwhale_api.moderation import mrf
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.music.serializers import COVER_WRITE_FIELD, CoverField
|
||||
from funkwhale_api.music import tasks
|
||||
|
||||
from funkwhale_api.tags import models as tags_models
|
||||
from funkwhale_api.tags import serializers as tags_serializers
|
||||
from funkwhale_api.users import serializers as users_serializers
|
||||
|
@ -246,7 +248,9 @@ class SimpleChannelArtistSerializer(serializers.Serializer):
|
|||
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
||||
cover = CoverField(allow_null=True, required=False)
|
||||
channel = serializers.UUIDField(allow_null=True, required=False)
|
||||
tracks_count = serializers.IntegerField(source="_tracks_count", required=False)
|
||||
tracks_count = serializers.IntegerField(
|
||||
source="_artist_credit__tracks_count", required=False
|
||||
)
|
||||
tags = serializers.ListField(
|
||||
child=serializers.CharField(), source="_prefetched_tagged_items", required=False
|
||||
)
|
||||
|
@ -749,7 +753,7 @@ class RssFeedItemSerializer(serializers.Serializer):
|
|||
else:
|
||||
existing_track = (
|
||||
music_models.Track.objects.filter(
|
||||
uuid=expected_uuid, artist__channel=channel
|
||||
uuid=expected_uuid, artist_credit__artist__channel=channel
|
||||
)
|
||||
.select_related("description", "attachment_cover")
|
||||
.first()
|
||||
|
@ -765,7 +769,6 @@ class RssFeedItemSerializer(serializers.Serializer):
|
|||
"disc_number": validated_data.get("itunes_season", 1) or 1,
|
||||
"position": validated_data.get("itunes_episode", 1) or 1,
|
||||
"title": validated_data["title"],
|
||||
"artist": channel.artist,
|
||||
}
|
||||
)
|
||||
if "rights" in validated_data:
|
||||
|
@ -801,6 +804,21 @@ class RssFeedItemSerializer(serializers.Serializer):
|
|||
**track_kwargs,
|
||||
defaults=track_defaults,
|
||||
)
|
||||
|
||||
# channel only have one artist so we can safely update artist_credit
|
||||
defaults = {
|
||||
"artist": channel.artist,
|
||||
"credit": channel.artist.name,
|
||||
"joinphrase": "",
|
||||
}
|
||||
query = (
|
||||
Q(artist=channel.artist) & Q(credit=channel.artist.name) & Q(joinphrase="")
|
||||
)
|
||||
artist_credit = tasks.get_best_candidate_or_create(
|
||||
music_models.ArtistCredit, query, defaults, ["artist", "joinphrase"]
|
||||
)
|
||||
track.artist_credit.set([artist_credit[0]])
|
||||
|
||||
# optimisation for reducing SQL queries, because we cannot use select_related with
|
||||
# update or create, so we restore the cache by hand
|
||||
if existing_track:
|
||||
|
|
|
@ -27,7 +27,7 @@ ARTIST_PREFETCH_QS = (
|
|||
"attachment_cover",
|
||||
)
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
.annotate(_tracks_count=Count("tracks"))
|
||||
.annotate(__count=Count("artist_credit__tracks"))
|
||||
)
|
||||
|
||||
|
||||
|
@ -103,7 +103,7 @@ class ChannelViewSet(
|
|||
queryset = super().get_queryset()
|
||||
if self.action == "retrieve":
|
||||
queryset = queryset.annotate(
|
||||
_downloads_count=Sum("artist__tracks__downloads_count")
|
||||
_downloads_count=Sum("artist__artist_credit__tracks__downloads_count")
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
@ -192,7 +192,6 @@ class ChannelViewSet(
|
|||
if object.attributed_to == actors.get_service_actor():
|
||||
# external feed, we redirect to the canonical one
|
||||
return http.HttpResponseRedirect(object.rss_url)
|
||||
|
||||
uploads = (
|
||||
object.library.uploads.playable_by(None)
|
||||
.prefetch_related(
|
||||
|
|
|
@ -68,22 +68,33 @@ def create_taggable_items(dependency):
|
|||
|
||||
|
||||
CONFIG = [
|
||||
{
|
||||
"id": "artist_credit",
|
||||
"model": music_models.ArtistCredit,
|
||||
"factory": "music.ArtistCredit",
|
||||
"factory_kwargs": {"joinphrase": ""},
|
||||
"depends_on": [
|
||||
{"field": "artist", "id": "artists", "default_factor": 0.5},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "tracks",
|
||||
"model": music_models.Track,
|
||||
"factory": "music.Track",
|
||||
"factory_kwargs": {"artist": None, "album": None},
|
||||
"factory_kwargs": {"album": None},
|
||||
"depends_on": [
|
||||
{"field": "album", "id": "albums", "default_factor": 0.1},
|
||||
{"field": "artist", "id": "artists", "default_factor": 0.05},
|
||||
{"field": "artist_credit", "id": "artist_credit", "default_factor": 0.05},
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "albums",
|
||||
"model": music_models.Album,
|
||||
"factory": "music.Album",
|
||||
"factory_kwargs": {"artist": None},
|
||||
"depends_on": [{"field": "artist", "id": "artists", "default_factor": 0.3}],
|
||||
"factory_kwargs": {},
|
||||
"depends_on": [
|
||||
{"field": "artist_credit", "id": "artist_credit", "default_factor": 0.3}
|
||||
],
|
||||
},
|
||||
{"id": "artists", "model": music_models.Artist, "factory": "music.Artist"},
|
||||
{
|
||||
|
@ -315,7 +326,12 @@ class Command(BaseCommand):
|
|||
value = random.choice(candidates)
|
||||
else:
|
||||
value = picked_objects[picked_pks[i]]
|
||||
setattr(obj, dependency["field"], value)
|
||||
if dependency["field"] == "artist_credit":
|
||||
# Direct assignment to the forward side of a many-to-many set is prohibited.
|
||||
# Use artist_credit.set() instead.
|
||||
continue
|
||||
else:
|
||||
setattr(obj, dependency["field"], value)
|
||||
if not handler:
|
||||
objects = row["model"].objects.bulk_create(objects, batch_size=BATCH_SIZE)
|
||||
results[row["id"]] = objects
|
||||
|
|
|
@ -40,9 +40,9 @@ def get_listen(track):
|
|||
release_name = track.album.title
|
||||
if track.album.mbid:
|
||||
additional_info["release_mbid"] = str(track.album.mbid)
|
||||
|
||||
if track.artist.mbid:
|
||||
additional_info["artist_mbids"] = [str(track.artist.mbid)]
|
||||
mbids = [ac.artist.mbid for ac in track.artist_credit.all() if ac.artist.mbid]
|
||||
if mbids:
|
||||
additional_info["artist_mbids"] = mbids
|
||||
|
||||
upload = track.uploads.filter(duration__gte=0).first()
|
||||
if upload:
|
||||
|
@ -50,7 +50,7 @@ def get_listen(track):
|
|||
|
||||
return liblistenbrainz.Listen(
|
||||
track_name=track.title,
|
||||
artist_name=track.artist.name,
|
||||
artist_name=track.get_artist_credit_string,
|
||||
listened_at=int(timezone.now()),
|
||||
release_name=release_name,
|
||||
additional_info=additional_info,
|
||||
|
|
|
@ -37,7 +37,7 @@ def get_payload(listening, api_key, conf):
|
|||
# See https://github.com/krateng/maloja/blob/master/API.md
|
||||
payload = {
|
||||
"key": api_key,
|
||||
"artists": [track.artist.name],
|
||||
"artists": [artist.name for artist in track.artist_credit.get_artists_list()],
|
||||
"title": track.title,
|
||||
"time": int(listening.creation_date.timestamp()),
|
||||
"nofix": bool(conf.get("nofix")),
|
||||
|
@ -46,8 +46,10 @@ def get_payload(listening, api_key, conf):
|
|||
if track.album:
|
||||
if track.album.title:
|
||||
payload["album"] = track.album.title
|
||||
if track.album.artist:
|
||||
payload["albumartists"] = [track.album.artist.name]
|
||||
if track.album.artist_credit.all():
|
||||
payload["albumartists"] = [
|
||||
artist.name for artist in track.album.artist_credit.get_artists_list()
|
||||
]
|
||||
|
||||
upload = track.uploads.filter(duration__gte=0).first()
|
||||
if upload:
|
||||
|
|
|
@ -84,7 +84,7 @@ def get_scrobble_payload(track, date, suffix="[0]"):
|
|||
"""
|
||||
upload = track.uploads.filter(duration__gte=0).first()
|
||||
data = {
|
||||
f"a{suffix}": track.artist.name,
|
||||
f"a{suffix}": track.get_a,
|
||||
f"t{suffix}": track.title,
|
||||
f"l{suffix}": upload.duration if upload else 0,
|
||||
f"b{suffix}": (track.album.title if track.album else "") or "",
|
||||
|
@ -103,7 +103,7 @@ def get_scrobble2_payload(track, date, suffix="[0]"):
|
|||
"""
|
||||
upload = track.uploads.filter(duration__gte=0).first()
|
||||
data = {
|
||||
"artist": track.artist.name,
|
||||
"artist": track.get_artist_credit_string,
|
||||
"track": track.title,
|
||||
"chosenByUser": 1,
|
||||
}
|
||||
|
|
|
@ -56,8 +56,11 @@ class TrackFavoriteViewSet(
|
|||
)
|
||||
tracks = Track.objects.with_playable_uploads(
|
||||
music_utils.get_actor_from_request(self.request)
|
||||
).select_related(
|
||||
"artist", "album__artist", "attributed_to", "album__attachment_cover"
|
||||
).prefetch_related(
|
||||
"artist_credit__artist",
|
||||
"album__artist_credit__artist",
|
||||
"attributed_to",
|
||||
"album__attachment_cover",
|
||||
)
|
||||
queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
||||
return queryset
|
||||
|
|
|
@ -293,6 +293,7 @@ CONTEXTS = [
|
|||
"Album": "fw:Album",
|
||||
"Track": "fw:Track",
|
||||
"Artist": "fw:Artist",
|
||||
"ArtistCredit": "fw:ArtistCredit",
|
||||
"Library": "fw:Library",
|
||||
"bitrate": {"@id": "fw:bitrate", "@type": "xsd:nonNegativeInteger"},
|
||||
"size": {"@id": "fw:size", "@type": "xsd:nonNegativeInteger"},
|
||||
|
@ -302,7 +303,15 @@ CONTEXTS = [
|
|||
"track": {"@id": "fw:track", "@type": "@id"},
|
||||
"cover": {"@id": "fw:cover", "@type": "as:Link"},
|
||||
"album": {"@id": "fw:album", "@type": "@id"},
|
||||
"artist": {"@id": "fw:artist", "@type": "@id"},
|
||||
"artists": {"@id": "fw:artists", "@type": "@id", "@container": "@list"},
|
||||
"artist_credit": {
|
||||
"@id": "fw:artist_credit",
|
||||
"@type": "@id",
|
||||
"@container": "@list",
|
||||
},
|
||||
"joinphrase": {"@id": "fw:joinphrase", "@type": "xsd:string"},
|
||||
"index": {"@id": "fw:index", "@type": "xsd:nonNegativeInteger"},
|
||||
"released": {"@id": "fw:released", "@type": "xsd:date"},
|
||||
"musicbrainzId": "fw:musicbrainzId",
|
||||
"license": {"@id": "fw:license", "@type": "@id"},
|
||||
|
|
|
@ -191,7 +191,6 @@ def prepare_for_serializer(payload, config, fallbacks={}):
|
|||
value = noop
|
||||
if not aliases:
|
||||
continue
|
||||
|
||||
for a in aliases:
|
||||
try:
|
||||
value = get_value(
|
||||
|
@ -279,7 +278,6 @@ class JsonLdSerializer(serializers.Serializer):
|
|||
for field in dereferenced_fields:
|
||||
for i in get_ids(data[field]):
|
||||
dereferenced_ids.add(i)
|
||||
|
||||
if dereferenced_ids:
|
||||
try:
|
||||
loop = asyncio.get_event_loop()
|
||||
|
|
|
@ -293,7 +293,7 @@ def inbox_delete_audio(payload, context):
|
|||
upload_fids = [payload["object"]["id"]]
|
||||
|
||||
query = Q(fid__in=upload_fids) & (
|
||||
Q(library__actor=actor) | Q(track__artist__channel__actor=actor)
|
||||
Q(library__actor=actor) | Q(track__artist_credit__artist__channel__actor=actor)
|
||||
)
|
||||
candidates = music_models.Upload.objects.filter(query)
|
||||
|
||||
|
@ -577,7 +577,9 @@ def inbox_delete_album(payload, context):
|
|||
logger.debug("Discarding deletion of empty library")
|
||||
return
|
||||
|
||||
query = Q(fid=album_id) & (Q(attributed_to=actor) | Q(artist__channel__actor=actor))
|
||||
query = Q(fid=album_id) & (
|
||||
Q(attributed_to=actor) | Q(artist_credit__artist__channel__actor=actor)
|
||||
)
|
||||
try:
|
||||
album = music_models.Album.objects.get(query)
|
||||
except music_models.Album.DoesNotExist:
|
||||
|
@ -590,9 +592,10 @@ def inbox_delete_album(payload, context):
|
|||
@outbox.register({"type": "Delete", "object.type": "Album"})
|
||||
def outbox_delete_album(context):
|
||||
album = context["album"]
|
||||
# album channels only have one artist but we might want to enhance this in the future
|
||||
actor = (
|
||||
album.artist.channel.actor
|
||||
if album.artist.get_channel()
|
||||
album.artist_credit.all()[0].artist.channel.actor
|
||||
if album.artist_credit.all()[0].artist.get_channel()
|
||||
else album.attributed_to
|
||||
)
|
||||
actor = actor or actors.get_service_actor()
|
||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import transaction
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework import serializers
|
||||
|
@ -1221,12 +1222,22 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
|||
self.updateable_fields, validated_data, instance
|
||||
)
|
||||
updated_fields = self.validate_updated_data(instance, updated_fields)
|
||||
|
||||
set_ac = False
|
||||
if "artist_credit" in updated_fields:
|
||||
artist_credit = updated_fields.pop("artist_credit")
|
||||
set_ac = True
|
||||
|
||||
if creating:
|
||||
instance, created = self.Meta.model.objects.get_or_create(
|
||||
fid=validated_data["id"], defaults=updated_fields
|
||||
)
|
||||
if set_ac:
|
||||
instance.artist_credit.set(artist_credit)
|
||||
else:
|
||||
music_tasks.update_library_entity(instance, updated_fields)
|
||||
obj = music_tasks.update_library_entity(instance, updated_fields)
|
||||
if set_ac:
|
||||
obj.artist_credit.set(artist_credit)
|
||||
|
||||
tags = [t["name"] for t in validated_data.get("tags", []) or []]
|
||||
tags_models.set_tags(instance, *tags)
|
||||
|
@ -1288,7 +1299,7 @@ class ArtistSerializer(MusicEntitySerializer):
|
|||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"released": jsonld.first_val(contexts.FW.released),
|
||||
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
|
||||
# "artist_credit": jsonld.first_attr(contexts.FW.artist_credit, "@list"),
|
||||
"image": jsonld.first_obj(contexts.AS.image),
|
||||
},
|
||||
)
|
||||
|
@ -1314,12 +1325,78 @@ class ArtistSerializer(MusicEntitySerializer):
|
|||
create = MusicEntitySerializer.update_or_create
|
||||
|
||||
|
||||
class ArtistCreditSerializer(MusicEntitySerializer):
|
||||
artist = MultipleSerializer(allowed=[BasicActorSerializer, ArtistSerializer])
|
||||
joinphrase = serializers.CharField(
|
||||
trim_whitespace=False, required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
credit = serializers.CharField(
|
||||
trim_whitespace=False, required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
|
||||
updateable_fields = [
|
||||
("name", "credit"),
|
||||
("artist", "artist"),
|
||||
("joinphrase", "joinphrase"),
|
||||
("musicbrainzId", "mbid"),
|
||||
("attributedTo", "attributed_to"),
|
||||
("published", "published"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = music_models.ArtistCredit
|
||||
jsonld_mapping = common_utils.concat_dicts(
|
||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"artist": jsonld.first_obj(contexts.FW.artist),
|
||||
"index": jsonld.first_val(contexts.FW.index),
|
||||
"joinphrase": jsonld.first_val(contexts.FW.joinphrase),
|
||||
},
|
||||
)
|
||||
|
||||
def to_representation(self, instance):
|
||||
if self.context.get("for_album", False):
|
||||
data = {
|
||||
"type": "ArtistCredit",
|
||||
"id": instance.fid,
|
||||
"artist": ArtistSerializer(
|
||||
instance.artist, context={"include_ap_context": False}
|
||||
).data,
|
||||
"joinphrase": instance.joinphrase,
|
||||
"name": instance.credit,
|
||||
"index": instance.index,
|
||||
"published": instance.creation_date.isoformat(),
|
||||
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
|
||||
# "attributedTo": instance.attributed_to.fid,
|
||||
}
|
||||
|
||||
elif (
|
||||
isinstance(instance, music_models.ArtistCreditQuerySet)
|
||||
and len(instance) == 1
|
||||
):
|
||||
instance = instance[0]
|
||||
|
||||
data = {
|
||||
"type": "ArtistCredit",
|
||||
"id": instance.fid,
|
||||
"artist": ArtistSerializer(
|
||||
instance.artist, context={"include_ap_context": False}
|
||||
).data,
|
||||
"joinphrase": instance.joinphrase,
|
||||
"name": instance.credit,
|
||||
"index": instance.index,
|
||||
"published": instance.creation_date.isoformat(),
|
||||
"musicbrainzId": str(instance.mbid) if instance.mbid else None,
|
||||
# "attributedTo": instance.attributed_to.fid,
|
||||
}
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
data["@context"] = jsonld.get_default_context()
|
||||
return data
|
||||
|
||||
|
||||
class AlbumSerializer(MusicEntitySerializer):
|
||||
released = serializers.DateField(allow_null=True, required=False)
|
||||
artists = serializers.ListField(
|
||||
child=MultipleSerializer(allowed=[BasicActorSerializer, ArtistSerializer]),
|
||||
min_length=1,
|
||||
)
|
||||
artist_credit = serializers.ListField(child=ArtistCreditSerializer(), min_length=1)
|
||||
image = ImageSerializer(
|
||||
allowed_mimetypes=["image/*"],
|
||||
allow_null=True,
|
||||
|
@ -1332,7 +1409,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
("musicbrainzId", "mbid"),
|
||||
("attributedTo", "attributed_to"),
|
||||
("released", "release_date"),
|
||||
("_artist", "artist"),
|
||||
("artist_credit", "artist_credit"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
|
@ -1341,13 +1418,13 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"released": jsonld.first_val(contexts.FW.released),
|
||||
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
|
||||
"artist_credit": jsonld.first_attr(contexts.FW.artist_credit, "@list"),
|
||||
"image": jsonld.first_obj(contexts.AS.image),
|
||||
},
|
||||
)
|
||||
|
||||
def to_representation(self, instance):
|
||||
d = {
|
||||
data = {
|
||||
"type": "Album",
|
||||
"id": instance.fid,
|
||||
"name": instance.title,
|
||||
|
@ -1361,42 +1438,59 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
else None,
|
||||
"tag": self.get_tags_repr(instance),
|
||||
}
|
||||
if instance.artist.get_channel():
|
||||
d["artists"] = [
|
||||
|
||||
# to do : album channel can only have one artist
|
||||
ac = instance.artist_credit.all()
|
||||
if len(ac) == 1 and ac[0].artist.get_channel():
|
||||
data["artist_credit"] = [
|
||||
{
|
||||
"type": instance.artist.channel.actor.type,
|
||||
"id": instance.artist.channel.actor.fid,
|
||||
"artist": {
|
||||
"type": ac[0].artist.channel.actor.type,
|
||||
"id": ac[0].artist.channel.actor.fid,
|
||||
},
|
||||
"joinphrase": "",
|
||||
"credit": ac[0].artist.name,
|
||||
}
|
||||
]
|
||||
data["artist_credit"] = ArtistCreditSerializer(
|
||||
instance.artist_credit.all(),
|
||||
context={"for_album": True},
|
||||
many=True,
|
||||
).data
|
||||
else:
|
||||
d["artists"] = [
|
||||
ArtistSerializer(
|
||||
instance.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
]
|
||||
include_content(d, instance.description)
|
||||
data["artist_credit"] = ArtistCreditSerializer(
|
||||
instance.artist_credit.all(),
|
||||
context={"include_ap_context": False},
|
||||
many=True,
|
||||
).data
|
||||
include_content(data, instance.description)
|
||||
if instance.attachment_cover:
|
||||
include_image(d, instance.attachment_cover)
|
||||
include_image(data, instance.attachment_cover)
|
||||
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
d["@context"] = jsonld.get_default_context()
|
||||
return d
|
||||
data["@context"] = jsonld.get_default_context()
|
||||
return data
|
||||
|
||||
def validate(self, data):
|
||||
validated_data = super().validate(data)
|
||||
if not self.parent:
|
||||
artist_data = validated_data["artists"][0]
|
||||
if artist_data.get("type", "Artist") == "Artist":
|
||||
validated_data["_artist"] = utils.retrieve_ap_object(
|
||||
artist_data["id"],
|
||||
actor=self.context.get("fetch_actor"),
|
||||
queryset=music_models.Artist,
|
||||
serializer_class=ArtistSerializer,
|
||||
)
|
||||
artist_credit_data = validated_data["artist_credit"]
|
||||
if artist_credit_data[0]["artist"].get("type", "Artist") == "Artist":
|
||||
acs = []
|
||||
for ac in validated_data["artist_credit"]:
|
||||
acs.append(
|
||||
utils.retrieve_ap_object(
|
||||
validated_data["artist_credit"][0]["id"],
|
||||
actor=self.context.get("fetch_actor"),
|
||||
queryset=music_models.ArtistCredit,
|
||||
serializer_class=ArtistCreditSerializer,
|
||||
)
|
||||
)
|
||||
validated_data["artist_credit"] = acs
|
||||
else:
|
||||
# we have an actor as an artist, so it's a channel
|
||||
actor = actors.get_actor(artist_data["id"])
|
||||
validated_data["_artist"] = actor.channel.artist
|
||||
actor = actors.get_actor(artist_credit_data[0]["artist"]["id"])
|
||||
validated_data["artist_credit"] = [{"artist": actor.channel.artist}]
|
||||
|
||||
return validated_data
|
||||
|
||||
|
@ -1406,7 +1500,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
|||
class TrackSerializer(MusicEntitySerializer):
|
||||
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
|
||||
disc = serializers.IntegerField(min_value=1, allow_null=True, required=False)
|
||||
artists = serializers.ListField(child=ArtistSerializer(), min_length=1)
|
||||
artist_credit = serializers.ListField(child=ArtistCreditSerializer(), min_length=1)
|
||||
album = AlbumSerializer()
|
||||
license = serializers.URLField(allow_null=True, required=False)
|
||||
copyright = serializers.CharField(allow_null=True, required=False)
|
||||
|
@ -1434,7 +1528,7 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||
{
|
||||
"album": jsonld.first_obj(contexts.FW.album),
|
||||
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
|
||||
"artist_credit": jsonld.first_attr(contexts.FW.artist_credit, "@list"),
|
||||
"copyright": jsonld.first_val(contexts.FW.copyright),
|
||||
"disc": jsonld.first_val(contexts.FW.disc),
|
||||
"license": jsonld.first_id(contexts.FW.license),
|
||||
|
@ -1444,7 +1538,7 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
)
|
||||
|
||||
def to_representation(self, instance):
|
||||
d = {
|
||||
data = {
|
||||
"type": "Track",
|
||||
"id": instance.fid,
|
||||
"name": instance.title,
|
||||
|
@ -1456,11 +1550,11 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
if instance.local_license
|
||||
else None,
|
||||
"copyright": instance.copyright if instance.copyright else None,
|
||||
"artists": [
|
||||
ArtistSerializer(
|
||||
instance.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"artist_credit": ArtistCreditSerializer(
|
||||
instance.artist_credit.all(),
|
||||
context={"include_ap_context": False},
|
||||
many=True,
|
||||
).data,
|
||||
"album": AlbumSerializer(
|
||||
instance.album, context={"include_ap_context": False}
|
||||
).data,
|
||||
|
@ -1469,11 +1563,11 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
else None,
|
||||
"tag": self.get_tags_repr(instance),
|
||||
}
|
||||
include_content(d, instance.description)
|
||||
include_image(d, instance.attachment_cover)
|
||||
include_content(data, instance.description)
|
||||
include_image(data, instance.attachment_cover)
|
||||
if self.context.get("include_ap_context", self.parent is None):
|
||||
d["@context"] = jsonld.get_default_context()
|
||||
return d
|
||||
data["@context"] = jsonld.get_default_context()
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
from funkwhale_api.music import tasks as music_tasks
|
||||
|
@ -1490,18 +1584,21 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
validated_data, "album.attributedTo", permissive=True
|
||||
)
|
||||
)
|
||||
artists = (
|
||||
common_utils.recursive_getattr(validated_data, "artists", permissive=True)
|
||||
artist_credit = (
|
||||
common_utils.recursive_getattr(
|
||||
validated_data, "artist_credit", permissive=True
|
||||
)
|
||||
or []
|
||||
)
|
||||
album_artists = (
|
||||
common_utils.recursive_getattr(
|
||||
validated_data, "album.artists", permissive=True
|
||||
validated_data, "album.artist_credit.all()", permissive=True
|
||||
)
|
||||
or []
|
||||
)
|
||||
for artist in artists + album_artists:
|
||||
actors_to_fetch.add(artist.get("attributedTo"))
|
||||
|
||||
for ac in artist_credit + album_artists:
|
||||
actors_to_fetch.add(ac["artist"].get("attributedTo"))
|
||||
|
||||
for url in actors_to_fetch:
|
||||
if not url:
|
||||
|
@ -1515,7 +1612,6 @@ class TrackSerializer(MusicEntitySerializer):
|
|||
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):
|
||||
|
@ -1780,7 +1876,7 @@ class ChannelOutboxSerializer(PaginatedCollectionSerializer):
|
|||
"actor": channel.actor,
|
||||
"items": channel.library.uploads.for_federation()
|
||||
.order_by("-creation_date")
|
||||
.filter(track__artist=channel.artist),
|
||||
.filter(track__artist_credit__artist=channel.artist),
|
||||
"type": "OrderedCollection",
|
||||
}
|
||||
r = super().to_representation(conf)
|
||||
|
@ -1850,7 +1946,7 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
|||
actor=actors.get_service_actor(),
|
||||
serializer_class=AlbumSerializer,
|
||||
queryset=music_models.Album.objects.filter(
|
||||
artist__channel=self.context["channel"]
|
||||
artist_credit__artist__channel=self.context["channel"]
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -1929,7 +2025,6 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
|||
now = timezone.now()
|
||||
track_defaults = {
|
||||
"fid": validated_data["id"],
|
||||
"artist": channel.artist,
|
||||
"position": validated_data.get("position", 1),
|
||||
"disc_number": validated_data.get("disc", 1),
|
||||
"title": validated_data["name"],
|
||||
|
@ -1941,9 +2036,32 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
|||
if validated_data.get("license"):
|
||||
track_defaults["license"] = licenses.match(validated_data["license"])
|
||||
|
||||
# to do : channel
|
||||
track, created = music_models.Track.objects.update_or_create(
|
||||
artist__channel=channel, fid=validated_data["id"], defaults=track_defaults
|
||||
# artist_credit__artist__channel=channel,
|
||||
fid=validated_data["id"],
|
||||
defaults=track_defaults,
|
||||
)
|
||||
query = (
|
||||
Q(
|
||||
artist=channel.artist,
|
||||
)
|
||||
& Q(credit__iexact=channel.artist.name)
|
||||
& Q(joinphrase="")
|
||||
)
|
||||
defaults = {
|
||||
"artist": channel.artist,
|
||||
"joinphrase": "",
|
||||
"credit": channel.artist.name,
|
||||
}
|
||||
|
||||
ac_obj = music_tasks.get_best_candidate_or_create(
|
||||
music_models.ArtistCredit,
|
||||
query,
|
||||
defaults=defaults,
|
||||
sort_fields=["mbid", "fid"],
|
||||
)
|
||||
track.artist_credit.set([ac_obj[0].id])
|
||||
|
||||
if "image" in validated_data:
|
||||
new_value = self.validated_data["image"]
|
||||
|
|
|
@ -17,6 +17,9 @@ router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
|||
music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries")
|
||||
music_router.register(r"uploads", views.MusicUploadViewSet, "uploads")
|
||||
music_router.register(r"artists", views.MusicArtistViewSet, "artists")
|
||||
# need to be here since the federation namespace need to be used by federation_utils.full_url()
|
||||
# we cannot have two namespace with the same name.
|
||||
music_router.register(r"artistcredit", views.MusicArtistCreditViewSet, "artistcredit")
|
||||
music_router.register(r"albums", views.MusicAlbumViewSet, "albums")
|
||||
music_router.register(r"tracks", views.MusicTrackViewSet, "tracks")
|
||||
|
||||
|
|
|
@ -161,7 +161,9 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
|||
"actor": channel.actor,
|
||||
"items": channel.library.uploads.for_federation()
|
||||
.order_by("-creation_date")
|
||||
.prefetch_related("library__channel__actor", "track__artist"),
|
||||
.prefetch_related(
|
||||
"library__channel__actor", "track__artist_credit__artist"
|
||||
),
|
||||
"item_serializer": serializers.ChannelCreateUploadSerializer,
|
||||
}
|
||||
return get_collection_response(
|
||||
|
@ -289,22 +291,22 @@ class MusicLibraryViewSet(
|
|||
.prefetch_related(
|
||||
Prefetch(
|
||||
"track",
|
||||
queryset=music_models.Track.objects.select_related(
|
||||
"album__artist__attributed_to",
|
||||
"artist__attributed_to",
|
||||
"artist__attachment_cover",
|
||||
queryset=music_models.Track.objects.prefetch_related(
|
||||
"album__artist_credit__artist__attributed_to",
|
||||
"artist_credit__artist__attributed_to",
|
||||
"artist_credit__artist__attachment_cover",
|
||||
"attachment_cover",
|
||||
"album__attributed_to",
|
||||
"attributed_to",
|
||||
"album__attachment_cover",
|
||||
"album__artist__attachment_cover",
|
||||
"album__artist_credit__artist__attachment_cover",
|
||||
"description",
|
||||
).prefetch_related(
|
||||
"tagged_items__tag",
|
||||
"album__tagged_items__tag",
|
||||
"album__artist__tagged_items__tag",
|
||||
"artist__tagged_items__tag",
|
||||
"artist__description",
|
||||
"album__artist_credit__artist__tagged_items__tag",
|
||||
"artist_credit__artist__tagged_items__tag",
|
||||
"artist_credit__artist__description",
|
||||
"album__description",
|
||||
),
|
||||
)
|
||||
|
@ -331,14 +333,14 @@ class MusicUploadViewSet(
|
|||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Upload.objects.local().select_related(
|
||||
queryset = music_models.Upload.objects.local().prefetch_related(
|
||||
"library__actor",
|
||||
"track__artist",
|
||||
"track__album__artist",
|
||||
"track__artist_credit__artist",
|
||||
"track__album__artist_credit__artist",
|
||||
"track__description",
|
||||
"track__album__attachment_cover",
|
||||
"track__album__artist__attachment_cover",
|
||||
"track__artist__attachment_cover",
|
||||
"track__album__artist_credit__artist__attachment_cover",
|
||||
"track__artist_credit__artist__attachment_cover",
|
||||
"track__attachment_cover",
|
||||
)
|
||||
serializer_class = serializers.UploadSerializer
|
||||
|
@ -393,13 +395,33 @@ class MusicArtistViewSet(
|
|||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class MusicArtistCreditViewSet(
|
||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.ArtistCredit.objects.local().prefetch_related("artist")
|
||||
serializer_class = serializers.ArtistCreditSerializer
|
||||
lookup_field = "uuid"
|
||||
|
||||
def retrieve(self, request, *args, **kwargs):
|
||||
instance = self.get_object()
|
||||
if utils.should_redirect_ap_to_html(request.headers.get("accept")):
|
||||
return redirect_to_html(instance.get_absolute_url())
|
||||
|
||||
serializer = self.get_serializer(instance)
|
||||
return response.Response(serializer.data)
|
||||
|
||||
|
||||
class MusicAlbumViewSet(
|
||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Album.objects.local().select_related(
|
||||
"artist__description", "description", "artist__attachment_cover"
|
||||
queryset = music_models.Album.objects.local().prefetch_related(
|
||||
"artist_credit__artist__description",
|
||||
"description",
|
||||
"artist_credit__artist__attachment_cover",
|
||||
)
|
||||
serializer_class = serializers.AlbumSerializer
|
||||
lookup_field = "uuid"
|
||||
|
@ -418,15 +440,15 @@ class MusicTrackViewSet(
|
|||
):
|
||||
authentication_classes = [authentication.SignatureAuthentication]
|
||||
renderer_classes = renderers.get_ap_renderers()
|
||||
queryset = music_models.Track.objects.local().select_related(
|
||||
"album__artist",
|
||||
queryset = music_models.Track.objects.local().prefetch_related(
|
||||
"album__artist_credit__artist",
|
||||
"album__description",
|
||||
"artist__description",
|
||||
"artist_credit__artist__description",
|
||||
"description",
|
||||
"attachment_cover",
|
||||
"album__artist__attachment_cover",
|
||||
"album__artist_credit__artist__attachment_cover",
|
||||
"album__attachment_cover",
|
||||
"artist__attachment_cover",
|
||||
"artist_credit__artist__attachment_cover",
|
||||
)
|
||||
serializer_class = serializers.TrackSerializer
|
||||
lookup_field = "uuid"
|
||||
|
|
|
@ -37,7 +37,7 @@ def get_content():
|
|||
|
||||
def get_top_music_categories():
|
||||
return (
|
||||
models.Track.objects.filter(artist__content_category="music")
|
||||
models.Track.objects.filter(artist_credit__artist__content_category="music")
|
||||
.exclude(tagged_items__tag_id=None)
|
||||
.values(name=F("tagged_items__tag__name"))
|
||||
.annotate(count=Count("name"))
|
||||
|
@ -47,7 +47,7 @@ def get_top_music_categories():
|
|||
|
||||
def get_top_podcast_categories():
|
||||
return (
|
||||
models.Track.objects.filter(artist__content_category="podcast")
|
||||
models.Track.objects.filter(artist_credit__artist__content_category="podcast")
|
||||
.exclude(tagged_items__tag_id=None)
|
||||
.values(name=F("tagged_items__tag__name"))
|
||||
.annotate(count=Count("name"))
|
||||
|
|
|
@ -96,12 +96,15 @@ class ManageAlbumFilterSet(filters.FilterSet):
|
|||
search_fields={
|
||||
"title": {"to": "title"},
|
||||
"fid": {"to": "fid"},
|
||||
"artist": {"to": "artist__name"},
|
||||
"artist": {"to": "artist_credit__artist__name"},
|
||||
"mbid": {"to": "mbid"},
|
||||
},
|
||||
filter_fields={
|
||||
"uuid": {"to": "uuid"},
|
||||
"artist_id": {"to": "artist_id", "field": forms.IntegerField()},
|
||||
"artist_id": {
|
||||
"to": "artist_credit__artist_id",
|
||||
"field": forms.IntegerField(),
|
||||
},
|
||||
"domain": {
|
||||
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
||||
},
|
||||
|
@ -117,7 +120,7 @@ class ManageAlbumFilterSet(filters.FilterSet):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
fields = ["title", "mbid", "fid", "artist"]
|
||||
fields = ["title", "mbid", "fid", "artist_credit"]
|
||||
|
||||
|
||||
class ManageTrackFilterSet(filters.FilterSet):
|
||||
|
@ -127,9 +130,9 @@ class ManageTrackFilterSet(filters.FilterSet):
|
|||
"title": {"to": "title"},
|
||||
"fid": {"to": "fid"},
|
||||
"mbid": {"to": "mbid"},
|
||||
"artist": {"to": "artist__name"},
|
||||
"artist": {"to": "artist_credit__artist__name"},
|
||||
"album": {"to": "album__title"},
|
||||
"album_artist": {"to": "album__artist__name"},
|
||||
"album_artist": {"to": "album__artist_credit__artist__name"},
|
||||
"copyright": {"to": "copyright"},
|
||||
},
|
||||
filter_fields={
|
||||
|
@ -156,7 +159,7 @@ class ManageTrackFilterSet(filters.FilterSet):
|
|||
|
||||
class Meta:
|
||||
model = music_models.Track
|
||||
fields = ["title", "mbid", "fid", "artist", "album", "license"]
|
||||
fields = ["title", "mbid", "fid", "artist_credit", "album", "license"]
|
||||
|
||||
|
||||
class ManageLibraryFilterSet(filters.FilterSet):
|
||||
|
|
|
@ -451,17 +451,25 @@ class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
|
|||
pass
|
||||
|
||||
|
||||
class ManageNestedArtistCreditSerializer(ManageBaseArtistSerializer):
|
||||
artist = ManageNestedArtistSerializer()
|
||||
|
||||
class Meta:
|
||||
model = music_models.ArtistCredit
|
||||
fields = ["artist"]
|
||||
|
||||
|
||||
class ManageAlbumSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageBaseAlbumSerializer
|
||||
):
|
||||
attributed_to = ManageBaseActorSerializer()
|
||||
artist = ManageNestedArtistSerializer()
|
||||
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||
tags = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
fields = ManageBaseAlbumSerializer.Meta.fields + [
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"attributed_to",
|
||||
"tags",
|
||||
"tracks_count",
|
||||
|
@ -477,17 +485,17 @@ class ManageAlbumSerializer(
|
|||
|
||||
|
||||
class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
|
||||
artist = ManageNestedArtistSerializer()
|
||||
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"]
|
||||
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist_credit"]
|
||||
|
||||
|
||||
class ManageTrackSerializer(
|
||||
music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer
|
||||
):
|
||||
artist = ManageNestedArtistSerializer()
|
||||
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||
album = ManageTrackAlbumSerializer(allow_null=True)
|
||||
attributed_to = ManageBaseActorSerializer(allow_null=True)
|
||||
uploads_count = serializers.SerializerMethodField()
|
||||
|
@ -497,7 +505,7 @@ class ManageTrackSerializer(
|
|||
class Meta:
|
||||
model = music_models.Track
|
||||
fields = ManageNestedTrackSerializer.Meta.fields + [
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"album",
|
||||
"attributed_to",
|
||||
"uploads_count",
|
||||
|
|
|
@ -84,8 +84,8 @@ class ManageArtistViewSet(
|
|||
music_models.Artist.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "attachment_cover", "channel")
|
||||
.annotate(_tracks_count=Count("tracks", distinct=True))
|
||||
.annotate(_albums_count=Count("albums", distinct=True))
|
||||
.annotate(_tracks_count=Count("artist_credit__tracks", distinct=True))
|
||||
.annotate(_albums_count=Count("artist_credit__albums", distinct=True))
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
)
|
||||
serializer_class = serializers.ManageArtistSerializer
|
||||
|
@ -98,7 +98,7 @@ class ManageArtistViewSet(
|
|||
def stats(self, request, *args, **kwargs):
|
||||
artist = self.get_object()
|
||||
tracks = music_models.Track.objects.filter(
|
||||
Q(artist=artist) | Q(album__artist=artist)
|
||||
Q(artist_credit__artist=artist) | Q(album__artist_credit__artist=artist)
|
||||
)
|
||||
data = get_stats(tracks, artist)
|
||||
return response.Response(data, status=200)
|
||||
|
@ -128,8 +128,8 @@ class ManageAlbumViewSet(
|
|||
queryset = (
|
||||
music_models.Album.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "artist", "attachment_cover")
|
||||
.prefetch_related("tracks")
|
||||
.select_related("attributed_to", "attachment_cover")
|
||||
.prefetch_related("tracks", "artist_credit__artist")
|
||||
)
|
||||
serializer_class = serializers.ManageAlbumSerializer
|
||||
filterset_class = filters.ManageAlbumFilterSet
|
||||
|
@ -177,10 +177,10 @@ class ManageTrackViewSet(
|
|||
queryset = (
|
||||
music_models.Track.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related(
|
||||
.prefetch_related(
|
||||
"attributed_to",
|
||||
"artist",
|
||||
"album__artist",
|
||||
"artist_credit",
|
||||
"album__artist_credit",
|
||||
"album__attachment_cover",
|
||||
"attachment_cover",
|
||||
)
|
||||
|
@ -273,11 +273,11 @@ class ManageLibraryViewSet(
|
|||
)
|
||||
artists = set(
|
||||
music_models.Album.objects.filter(pk__in=albums).values_list(
|
||||
"artist", flat=True
|
||||
"artist_credit__artist", flat=True
|
||||
)
|
||||
) | set(
|
||||
music_models.Track.objects.filter(pk__in=tracks).values_list(
|
||||
"artist", flat=True
|
||||
"artist_credit__artist", flat=True
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -313,7 +313,11 @@ class ManageUploadViewSet(
|
|||
queryset = (
|
||||
music_models.Upload.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("library__actor", "track__artist", "track__album__artist")
|
||||
.prefetch_related(
|
||||
"library__actor",
|
||||
"track__artist_credit__artist",
|
||||
"track__album__artist_credit__artist",
|
||||
)
|
||||
)
|
||||
serializer_class = serializers.ManageUploadSerializer
|
||||
filterset_class = filters.ManageUploadFilterSet
|
||||
|
@ -702,8 +706,8 @@ class ManageChannelViewSet(
|
|||
music_models.Artist.objects.all()
|
||||
.order_by("-id")
|
||||
.select_related("attributed_to", "attachment_cover", "channel")
|
||||
.annotate(_tracks_count=Count("tracks"))
|
||||
.annotate(_albums_count=Count("albums"))
|
||||
.annotate(_tracks_count=Count("artist_credit__tracks"))
|
||||
.annotate(_albums_count=Count("artist_credit__albums"))
|
||||
.prefetch_related(music_views.TAG_PREFETCH)
|
||||
),
|
||||
)
|
||||
|
@ -719,7 +723,8 @@ class ManageChannelViewSet(
|
|||
def stats(self, request, *args, **kwargs):
|
||||
channel = self.get_object()
|
||||
tracks = music_models.Track.objects.filter(
|
||||
Q(artist=channel.artist) | Q(album__artist=channel.artist)
|
||||
Q(artist_credit__artist=channel.artist)
|
||||
| Q(album__artist_credit__artist=channel.artist)
|
||||
)
|
||||
data = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
|
||||
data["follows"] = channel.actor.received_follows.count()
|
||||
|
|
|
@ -4,11 +4,24 @@ from django_filters import rest_framework as filters
|
|||
USER_FILTER_CONFIG = {
|
||||
"ARTIST": {"target_artist": ["pk"]},
|
||||
"CHANNEL": {"target_artist": ["artist__pk"]},
|
||||
"ALBUM": {"target_artist": ["artist__pk"]},
|
||||
"TRACK": {"target_artist": ["artist__pk", "album__artist__pk"]},
|
||||
"LISTENING": {"target_artist": ["track__album__artist__pk", "track__artist__pk"]},
|
||||
"ALBUM": {"target_artist": ["artist_credit__artist__pk"]},
|
||||
"TRACK": {
|
||||
"target_artist": [
|
||||
"artist_credit__artist__pk",
|
||||
"album__artist_credit__artist__pk",
|
||||
]
|
||||
},
|
||||
"LISTENING": {
|
||||
"target_artist": [
|
||||
"track__album__artist_credit__artist__pk",
|
||||
"track__artist_credit__artist__pk",
|
||||
]
|
||||
},
|
||||
"TRACK_FAVORITE": {
|
||||
"target_artist": ["track__album__artist__pk", "track__artist__pk"]
|
||||
"target_artist": [
|
||||
"track__album__artist_credit__artist__pk",
|
||||
"track__artist_credit__artist__pk",
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -89,10 +89,29 @@ class ArtistStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
|||
]
|
||||
|
||||
|
||||
@state_serializers.register(name="music.ArtistCredit")
|
||||
class ArtistCreditStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
artist = ArtistStateSerializer()
|
||||
|
||||
class Meta:
|
||||
model = music_models.ArtistCredit
|
||||
fields = [
|
||||
"id",
|
||||
"credit",
|
||||
"mbid",
|
||||
"fid",
|
||||
"creation_date",
|
||||
"uuid",
|
||||
"artist",
|
||||
"joinphrase",
|
||||
"index",
|
||||
]
|
||||
|
||||
|
||||
@state_serializers.register(name="music.Album")
|
||||
class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
tags = TAGS_FIELD
|
||||
artist = ArtistStateSerializer()
|
||||
artist_credit = ArtistCreditStateSerializer(many=True)
|
||||
|
||||
class Meta:
|
||||
model = music_models.Album
|
||||
|
@ -103,7 +122,7 @@ class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
|||
"fid",
|
||||
"creation_date",
|
||||
"uuid",
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"release_date",
|
||||
"tags",
|
||||
"description",
|
||||
|
@ -113,7 +132,7 @@ class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
|||
@state_serializers.register(name="music.Track")
|
||||
class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||
tags = TAGS_FIELD
|
||||
artist = ArtistStateSerializer()
|
||||
artist_credit = ArtistCreditStateSerializer(many=True)
|
||||
album = AlbumStateSerializer()
|
||||
|
||||
class Meta:
|
||||
|
@ -125,7 +144,7 @@ class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
|||
"fid",
|
||||
"creation_date",
|
||||
"uuid",
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"album",
|
||||
"disc_number",
|
||||
"position",
|
||||
|
@ -230,6 +249,7 @@ TARGET_CONFIG = {
|
|||
"id_field": serializers.UUIDField(),
|
||||
},
|
||||
"artist": {"queryset": music_models.Artist.objects.all()},
|
||||
"artist_credit": {"queryset": music_models.ArtistCredit.objects.all()},
|
||||
"album": {"queryset": music_models.Album.objects.all()},
|
||||
"track": {"queryset": music_models.Track.objects.all()},
|
||||
"library": {
|
||||
|
|
|
@ -3,6 +3,17 @@ from funkwhale_api.common import admin
|
|||
from . import models
|
||||
|
||||
|
||||
@admin.register(models.ArtistCredit)
|
||||
class ArtistCreditAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
"artist",
|
||||
"credit",
|
||||
"joinphrase",
|
||||
"creation_date",
|
||||
]
|
||||
search_fields = ["artist__name", "credit"]
|
||||
|
||||
|
||||
@admin.register(models.Artist)
|
||||
class ArtistAdmin(admin.ModelAdmin):
|
||||
list_display = ["name", "mbid", "creation_date", "modification_date"]
|
||||
|
@ -11,15 +22,15 @@ class ArtistAdmin(admin.ModelAdmin):
|
|||
|
||||
@admin.register(models.Album)
|
||||
class AlbumAdmin(admin.ModelAdmin):
|
||||
list_display = ["title", "artist", "mbid", "release_date", "creation_date"]
|
||||
search_fields = ["title", "artist__name", "mbid"]
|
||||
list_display = ["title", "mbid", "release_date", "creation_date"]
|
||||
search_fields = ["title", "mbid"]
|
||||
list_select_related = True
|
||||
|
||||
|
||||
@admin.register(models.Track)
|
||||
class TrackAdmin(admin.ModelAdmin):
|
||||
list_display = ["title", "artist", "album", "mbid"]
|
||||
search_fields = ["title", "artist__name", "album__title", "mbid"]
|
||||
list_display = ["title", "album", "mbid"]
|
||||
search_fields = ["title", "album__title", "mbid"]
|
||||
list_select_related = ["album__artist", "artist"]
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from dynamic_preferences import types
|
||||
from dynamic_preferences.registries import global_preferences_registry
|
||||
from django.forms import widgets
|
||||
|
||||
music = types.Section("music")
|
||||
|
||||
|
@ -47,3 +48,44 @@ class MbidTaggedContent(types.BooleanPreference):
|
|||
"or enable quality filtering to hide untagged content from API calls. "
|
||||
)
|
||||
default = False
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class JoinPhrases(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = music
|
||||
name = "join_phrases"
|
||||
verbose_name = "Join Phrases"
|
||||
help_text = (
|
||||
"Used by the artist parser to create multiples artists in case the metadata "
|
||||
"is a single string. BE WARNED, changing this can break the parser in unexpected ways. "
|
||||
"It's MANDATORY to escape dots and to put doted variation before because the first match is used "
|
||||
"(example : `|feat\.|ft\.|feat|` and not `feat|feat\.|ft\.|feat`.). ORDER is really important "
|
||||
"(says an anarchist). To avoid artist duplication and wrongly parsed artist data "
|
||||
"it's recommended to tag files with Musicbrainz Picard."
|
||||
)
|
||||
default = (
|
||||
"featuring | feat\. | ft\. | feat | with | and | & | &|& |&| vs\. | \| | \||\| |\|| , | ,|, |,|"
|
||||
" ; | ;|; |;| versus | vs | \( | \(|\( |\(| Remix\) |Remix\) | Remix\)| \) | \)|\) |\)| x |"
|
||||
"accompanied by | alongside | together with | collaboration with | featuring special guest |"
|
||||
"joined by | joined with | featuring guest | introducing | accompanied by | performed by | performed with |"
|
||||
"performed by and | and | featuring | with | presenting | accompanied by | and special guest |"
|
||||
"featuring special guests | featuring and | featuring & | and featuring "
|
||||
)
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
||||
|
||||
@global_preferences_registry.register
|
||||
class DefaultJoinPhrases(types.StringPreference):
|
||||
show_in_api = True
|
||||
section = music
|
||||
name = "default_join_phrase"
|
||||
verbose_name = "Default Join Phrase"
|
||||
help_text = (
|
||||
"The default join phrase used by artist parser"
|
||||
"For exemple: `artists = [artist1, Artist2]` will be displayed has : artist1.name;artis2.name"
|
||||
)
|
||||
default = ";"
|
||||
widget = widgets.Textarea
|
||||
field_kwargs = {"required": False}
|
||||
|
|
|
@ -2,6 +2,8 @@ import os
|
|||
|
||||
import factory
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from funkwhale_api.common import factories as common_factories
|
||||
from funkwhale_api.factories import NoUpdateOnCreate, registry
|
||||
from funkwhale_api.federation import factories as federation_factories
|
||||
|
@ -9,6 +11,8 @@ from funkwhale_api.music import licenses
|
|||
from funkwhale_api.tags import factories as tags_factories
|
||||
from funkwhale_api.users import factories as users_factories
|
||||
|
||||
from urllib.parse import urlparse
|
||||
|
||||
SAMPLES_PATH = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
"tests",
|
||||
|
@ -62,7 +66,7 @@ class ArtistFactory(
|
|||
name = factory.Faker("name")
|
||||
mbid = factory.Faker("uuid4")
|
||||
fid = factory.Faker("federation_url")
|
||||
playable = playable_factory("track__album__artist")
|
||||
playable = playable_factory("track__album__artist_credit__artist")
|
||||
|
||||
class Meta:
|
||||
model = "music.Artist"
|
||||
|
@ -77,6 +81,16 @@ class ArtistFactory(
|
|||
)
|
||||
|
||||
|
||||
@registry.register
|
||||
class ArtistCreditFactory(factory.django.DjangoModelFactory):
|
||||
artist = factory.SubFactory(ArtistFactory)
|
||||
credit = factory.LazyAttribute(lambda obj: obj.artist.name)
|
||||
joinphrase = ""
|
||||
|
||||
class Meta:
|
||||
model = "music.ArtistCredit"
|
||||
|
||||
|
||||
@registry.register
|
||||
class AlbumFactory(
|
||||
tags_factories.TaggableFactory, NoUpdateOnCreate, factory.django.DjangoModelFactory
|
||||
|
@ -84,7 +98,6 @@ class AlbumFactory(
|
|||
title = factory.Faker("sentence", nb_words=3)
|
||||
mbid = factory.Faker("uuid4")
|
||||
release_date = factory.Faker("date_object")
|
||||
artist = factory.SubFactory(ArtistFactory)
|
||||
release_group_id = factory.Faker("uuid4")
|
||||
fid = factory.Faker("federation_url")
|
||||
playable = playable_factory("track__album")
|
||||
|
@ -96,14 +109,22 @@ class AlbumFactory(
|
|||
attributed = factory.Trait(
|
||||
attributed_to=factory.SubFactory(federation_factories.ActorFactory)
|
||||
)
|
||||
|
||||
local = factory.Trait(
|
||||
fid=factory.Faker("federation_url", local=True), artist__local=True
|
||||
fid=factory.Faker("federation_url", local=True),
|
||||
)
|
||||
with_cover = factory.Trait(
|
||||
attachment_cover=factory.SubFactory(common_factories.AttachmentFactory)
|
||||
)
|
||||
|
||||
@factory.post_generation
|
||||
def artist_credit(self, create, extracted, **kwargs):
|
||||
if urlparse(self.fid).netloc == settings.FEDERATION_HOSTNAME:
|
||||
kwargs["artist__local"] = True
|
||||
if extracted:
|
||||
self.artist_credit.add(extracted)
|
||||
if create:
|
||||
self.artist_credit.add(ArtistCreditFactory(**kwargs))
|
||||
|
||||
|
||||
@registry.register
|
||||
class TrackFactory(
|
||||
|
@ -132,22 +153,29 @@ class TrackFactory(
|
|||
)
|
||||
|
||||
@factory.post_generation
|
||||
def artist(self, created, extracted, **kwargs):
|
||||
def artist_credit(self, created, extracted, **kwargs):
|
||||
"""
|
||||
A bit intricated, because we want to be able to specify a different
|
||||
track artist with a fallback on album artist if nothing is specified.
|
||||
|
||||
And handle cases where build or build_batch are used (so no db calls)
|
||||
"""
|
||||
# needed to get a primary key on the track and album objects. The primary key is needed for many_to_many
|
||||
if self.album:
|
||||
self.album.save()
|
||||
|
||||
if not self.pk:
|
||||
self.save()
|
||||
|
||||
if extracted:
|
||||
self.artist = extracted
|
||||
self.artist_credit.add(extracted)
|
||||
elif kwargs:
|
||||
if created:
|
||||
self.artist = ArtistFactory(**kwargs)
|
||||
self.artist_credit.add(ArtistCreditFactory(**kwargs))
|
||||
else:
|
||||
self.artist = ArtistFactory.build(**kwargs)
|
||||
self.artist_credit.add(ArtistCreditFactory.build(**kwargs))
|
||||
elif self.album:
|
||||
self.artist = self.album.artist
|
||||
self.artist_credit.set(self.album.artist_credit.all())
|
||||
if created:
|
||||
self.save()
|
||||
|
||||
|
@ -194,7 +222,9 @@ class UploadFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
|||
from funkwhale_api.audio import factories as audio_factories
|
||||
|
||||
audio_factories.ChannelFactory(
|
||||
library=self.library, artist=self.track.artist, **kwargs
|
||||
library=self.library,
|
||||
artist=self.track.artist_credit.all()[0].artist,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -88,6 +88,37 @@ class LibraryFilterSet(filters.FilterSet):
|
|||
return qs
|
||||
|
||||
|
||||
class ArtistCreditFilter(moderation_filters.HiddenContentFilterSet):
|
||||
q = fields.SearchFilter(search_fields=["credit"])
|
||||
|
||||
credited_artist = filters.CharFilter(field_name="_", method="filter_artist_credit")
|
||||
|
||||
# def filter_artist_credit(self, queryset, name, value):
|
||||
# return models.ArtistCredit.objects.all().filter(artist=value)
|
||||
# library = filters.CharFilter(field_name="_", method="filter_library")
|
||||
|
||||
def filter_artist_credit(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset
|
||||
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
artist = models.Artist.objects.get(pk=value)
|
||||
if not artist:
|
||||
return queryset.none()
|
||||
|
||||
# uploads = models.Upload.objects.filter(track__artist_credit__artist=artist)
|
||||
|
||||
# uploads = uploads.playable_by(actor)
|
||||
# ids = uploads.values_list(self.Meta.artist_credit_filter_field, flat=True)
|
||||
qs = queryset.filter(artist_credit__artist=artist)
|
||||
|
||||
return qs
|
||||
|
||||
class Meta:
|
||||
model = models.ArtistCredit
|
||||
fields = ["credit", "artist"]
|
||||
|
||||
|
||||
class ArtistFilter(
|
||||
RelatedFilterSet,
|
||||
LibraryFilterSet,
|
||||
|
@ -123,7 +154,7 @@ class ArtistFilter(
|
|||
}
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
||||
include_channels_field = "channel"
|
||||
library_filter_field = "track__artist"
|
||||
library_filter_field = "track__artist_credit__artist"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
|
@ -137,12 +168,21 @@ class TrackFilter(
|
|||
RelatedFilterSet,
|
||||
ChannelFilterSet,
|
||||
LibraryFilterSet,
|
||||
ArtistCreditFilter,
|
||||
audio_filters.IncludeChannelsFilterSet,
|
||||
moderation_filters.HiddenContentFilterSet,
|
||||
):
|
||||
q = fields.SearchFilter(
|
||||
search_fields=["title", "album__title", "artist__name"],
|
||||
fts_search_fields=["body_text", "artist__body_text", "album__body_text"],
|
||||
search_fields=[
|
||||
"title",
|
||||
"album__title",
|
||||
"artist_credit__artist__name",
|
||||
],
|
||||
fts_search_fields=[
|
||||
"body_text",
|
||||
"artist_credit__artist__body_text",
|
||||
"album__body_text",
|
||||
],
|
||||
)
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
tag = TAG_FILTER
|
||||
|
@ -165,8 +205,11 @@ class TrackFilter(
|
|||
("size", "size"),
|
||||
("position", "position"),
|
||||
("disc_number", "disc_number"),
|
||||
("artist__name", "artist__name"),
|
||||
("artist__modification_date", "artist__modification_date"),
|
||||
("artist_credit__artist__name", "artist_credit__artist__name"),
|
||||
(
|
||||
"artist_credit__artist__modification_date",
|
||||
"artist_credit__artist__modification_date",
|
||||
),
|
||||
("?", "random"),
|
||||
("tag_matches", "related"),
|
||||
)
|
||||
|
@ -182,24 +225,27 @@ class TrackFilter(
|
|||
"mbid": ["exact"],
|
||||
}
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
|
||||
include_channels_field = "artist__channel"
|
||||
include_channels_field = "artist_credit__artist__channel"
|
||||
channel_filter_field = "track"
|
||||
library_filter_field = "track"
|
||||
artist_credit_filter_field = "artist__credit__artist"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
return queryset.playable_by(actor, value).distinct()
|
||||
|
||||
def filter_artist(self, queryset, name, value):
|
||||
return queryset.filter(Q(artist=value) | Q(album__artist=value))
|
||||
return queryset.filter(
|
||||
Q(artist_credit__artist=value) | Q(album__artist_credit__artist=value)
|
||||
)
|
||||
|
||||
|
||||
class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||
library = filters.CharFilter("library__uuid")
|
||||
channel = filters.CharFilter("library__channel__uuid")
|
||||
track = filters.UUIDFilter("track__uuid")
|
||||
track_artist = filters.UUIDFilter("track__artist__uuid")
|
||||
album_artist = filters.UUIDFilter("track__album__artist__uuid")
|
||||
track_artist = filters.UUIDFilter("track__artist_credit__artist__uuid")
|
||||
album_artist = filters.UUIDFilter("track__album__artist_credit__artist__uuid")
|
||||
library = filters.UUIDFilter("library__uuid")
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
scope = common_filters.ActorScopeFilter(
|
||||
|
@ -233,7 +279,7 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
|||
"mimetype",
|
||||
"import_reference",
|
||||
]
|
||||
include_channels_field = "track__artist__channel"
|
||||
include_channels_field = "track__artist_credit__artist__channel"
|
||||
|
||||
def filter_playable(self, queryset, name, value):
|
||||
actor = utils.get_actor_from_request(self.request)
|
||||
|
@ -249,10 +295,10 @@ class AlbumFilter(
|
|||
):
|
||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||
q = fields.SearchFilter(
|
||||
search_fields=["title", "artist__name"],
|
||||
fts_search_fields=["body_text", "artist__body_text"],
|
||||
search_fields=["title", "artist_credit__artist__name"],
|
||||
fts_search_fields=["body_text", "artist_credit__artist__body_text"],
|
||||
)
|
||||
content_category = filters.CharFilter("artist__content_category")
|
||||
content_category = filters.CharFilter("artist_credit__artist__content_category")
|
||||
tag = TAG_FILTER
|
||||
scope = common_filters.ActorScopeFilter(
|
||||
actor_field="tracks__uploads__library__actor",
|
||||
|
@ -265,7 +311,10 @@ class AlbumFilter(
|
|||
("creation_date", "creation_date"),
|
||||
("release_date", "release_date"),
|
||||
("title", "title"),
|
||||
("artist__modification_date", "artist__modification_date"),
|
||||
(
|
||||
"artist_credit__artist__modification_date",
|
||||
"artist_credit__artist__modification_date",
|
||||
),
|
||||
("?", "random"),
|
||||
("tag_matches", "related"),
|
||||
)
|
||||
|
@ -273,9 +322,9 @@ class AlbumFilter(
|
|||
|
||||
class Meta:
|
||||
model = models.Album
|
||||
fields = ["artist", "mbid"]
|
||||
fields = ["artist_credit", "mbid"]
|
||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
|
||||
include_channels_field = "artist__channel"
|
||||
include_channels_field = "artist_credit__artist__channel"
|
||||
channel_filter_field = "track__album"
|
||||
library_filter_field = "track__album"
|
||||
|
||||
|
|
|
@ -12,11 +12,14 @@ class Importer:
|
|||
|
||||
def load(self, cleaned_data, raw_data, import_hooks):
|
||||
mbid = cleaned_data.pop("mbid")
|
||||
artists_credits = cleaned_data.pop("artist_credit", None)
|
||||
# let's validate data, just in case
|
||||
instance = self.model(**cleaned_data)
|
||||
exclude = EXCLUDE_VALIDATION.get(self.model.__name__, [])
|
||||
instance.full_clean(exclude=["mbid", "uuid", "fid", "from_activity"] + exclude)
|
||||
m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0]
|
||||
if artists_credits:
|
||||
m.artist_credit.set(artists_credits)
|
||||
for hook in import_hooks:
|
||||
hook(m, cleaned_data, raw_data)
|
||||
return m
|
||||
|
@ -47,4 +50,9 @@ class Mapping:
|
|||
)
|
||||
|
||||
|
||||
registry = {"Artist": Importer, "Track": Importer, "Album": Importer}
|
||||
registry = {
|
||||
"Artist": Importer,
|
||||
"ArtistCredit": Importer,
|
||||
"Track": Importer,
|
||||
"Album": Importer,
|
||||
}
|
||||
|
|
|
@ -702,8 +702,8 @@ def handle_modified(event, stdout, library, in_place, **kwargs):
|
|||
.filter(source=source)
|
||||
.select_related(
|
||||
"track__attributed_to",
|
||||
"track__artist",
|
||||
"track__album__artist",
|
||||
"track__artist_credit__artist",
|
||||
"track__album__artist_credit__artist",
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
@ -839,9 +839,9 @@ def check_upload(stdout, upload):
|
|||
" Cannot update track metadata, track belongs to someone else"
|
||||
)
|
||||
else:
|
||||
track = models.Track.objects.select_related("artist", "album__artist").get(
|
||||
pk=upload.track_id
|
||||
)
|
||||
track = models.Track.objects.select_related(
|
||||
"artist_credit__artist", "album__artist_credit__artist"
|
||||
).get(pk=upload.track_id)
|
||||
try:
|
||||
tasks.update_track_metadata(upload.get_metadata(), track)
|
||||
except serializers.ValidationError as e:
|
||||
|
|
|
@ -495,55 +495,61 @@ class ArtistField(serializers.Field):
|
|||
return final
|
||||
|
||||
def to_internal_value(self, data):
|
||||
# we have multiple values that can be separated by various separators
|
||||
separators = [";", ","]
|
||||
from . import tasks
|
||||
|
||||
# we have multiple mbid values that can be separated by various separators
|
||||
separators = [";", ",", "/"]
|
||||
artists_credits_tuples = None
|
||||
# we get a list like that if tagged via musicbrainz
|
||||
# ae29aae4-abfb-4609-8f54-417b1f4d64cc; 3237b5a8-ae44-400c-aa6d-cea51f0b9074;
|
||||
raw_mbids = data["mbids"]
|
||||
used_separator = None
|
||||
mbids = [raw_mbids]
|
||||
if raw_mbids:
|
||||
if "/" in raw_mbids:
|
||||
# it's a featuring, we can't handle this now
|
||||
mbids = []
|
||||
else:
|
||||
for separator in separators:
|
||||
if separator in raw_mbids:
|
||||
used_separator = separator
|
||||
mbids = [m.strip() for m in raw_mbids.split(separator)]
|
||||
break
|
||||
|
||||
# now, we split on artist names, using the same separator as the one used
|
||||
# by mbids, if any
|
||||
names = []
|
||||
|
||||
if data.get("artists", None):
|
||||
if raw_mbids:
|
||||
for separator in separators:
|
||||
if separator in data["artists"]:
|
||||
names = [n.strip() for n in data["artists"].split(separator)]
|
||||
if separator in raw_mbids:
|
||||
mbids = [m.strip() for m in raw_mbids.split(separator)]
|
||||
break
|
||||
# corner case: 'album artist' field with only one artist but multiple names in 'artits' field
|
||||
if (
|
||||
not names
|
||||
and data.get("names", None)
|
||||
and any(separator in data["names"] for separator in separators)
|
||||
):
|
||||
names = [n.strip() for n in data["names"].split(separators[0])]
|
||||
elif not names:
|
||||
names = [data["artists"]]
|
||||
elif used_separator and mbids:
|
||||
names = [n.strip() for n in data["names"].split(used_separator)]
|
||||
|
||||
# now, we split on artist names
|
||||
|
||||
names_artists_credits_tuples = (
|
||||
tasks.parse_credits(data["names"], None, None)
|
||||
if data.get("names", False)
|
||||
else []
|
||||
)
|
||||
artist_artists_credits_tuples = (
|
||||
tasks.parse_credits(data["artists"], None, None)
|
||||
if data.get("artists", False)
|
||||
else []
|
||||
)
|
||||
if (
|
||||
raw_mbids
|
||||
and len(names_artists_credits_tuples) != len(mbids)
|
||||
and len(artist_artists_credits_tuples) != len(mbids)
|
||||
):
|
||||
logger.info(
|
||||
f"Error parsing artist data, not the same amount of mbids and parsed artists. Trying to proceed anyway"
|
||||
)
|
||||
|
||||
if len(names_artists_credits_tuples) > len(artist_artists_credits_tuples):
|
||||
artists_credits_tuples = names_artists_credits_tuples
|
||||
else:
|
||||
names = [data["names"]]
|
||||
artists_credits_tuples = artist_artists_credits_tuples
|
||||
|
||||
final = []
|
||||
for i, name in enumerate(names):
|
||||
try:
|
||||
mbid = mbids[i]
|
||||
except IndexError:
|
||||
mbid = None
|
||||
artist = {"name": name, "mbid": mbid}
|
||||
for i, ac in enumerate(artists_credits_tuples):
|
||||
artist = {
|
||||
"name": ac[0],
|
||||
"mbid": (mbids[i] if 0 <= i < len(mbids) else None),
|
||||
"joinphrase": ac[1],
|
||||
"index": i,
|
||||
}
|
||||
final.append(artist)
|
||||
|
||||
if len(artists_credits_tuples) == 0:
|
||||
final = []
|
||||
|
||||
field = serializers.ListField(
|
||||
child=ArtistSerializer(strict=self.context.get("strict", True)),
|
||||
min_length=1,
|
||||
|
@ -682,6 +688,9 @@ class MBIDField(serializers.UUIDField):
|
|||
class ArtistSerializer(serializers.Serializer):
|
||||
name = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||
mbid = MBIDField()
|
||||
joinphrase = serializers.CharField(
|
||||
trim_whitespace=False, required=False, allow_null=True, allow_blank=True
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.strict = kwargs.pop("strict", True)
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
# Generated by Django 4.2.9 on 2024-03-16 00:36
|
||||
|
||||
import django.contrib.postgres.search
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
def skip(apps, schema_editor):
|
||||
pass
|
||||
|
||||
|
||||
def set_artist_credit(apps, schema_editor):
|
||||
Track = apps.get_model("music", "Track")
|
||||
Album = apps.get_model("music", "Album")
|
||||
ArtistCredit = apps.get_model("music", "ArtistCredit")
|
||||
|
||||
for obj_manager in (Track, Album):
|
||||
for obj in obj_manager.objects.all():
|
||||
artist_credit, created = ArtistCredit.objects.get_or_create(
|
||||
artist=obj.artist,
|
||||
joinphrase="",
|
||||
credit=obj.artist.name,
|
||||
)
|
||||
|
||||
obj.artist_credit.set([artist_credit])
|
||||
obj.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("music", "0057_auto_20221118_2108"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ArtistCredit",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"fid",
|
||||
models.URLField(
|
||||
db_index=True, max_length=500, null=True, unique=True
|
||||
),
|
||||
),
|
||||
(
|
||||
"mbid",
|
||||
models.UUIDField(blank=True, db_index=True, null=True, unique=True),
|
||||
),
|
||||
(
|
||||
"uuid",
|
||||
models.UUIDField(db_index=True, default=uuid.uuid4, unique=True),
|
||||
),
|
||||
(
|
||||
"creation_date",
|
||||
models.DateTimeField(
|
||||
db_index=True, default=django.utils.timezone.now
|
||||
),
|
||||
),
|
||||
(
|
||||
"body_text",
|
||||
django.contrib.postgres.search.SearchVectorField(blank=True),
|
||||
),
|
||||
("credit", models.CharField(blank=True, max_length=500, null=True)),
|
||||
("joinphrase", models.CharField(blank=True, max_length=250, null=True)),
|
||||
("index", models.IntegerField(blank=True, null=True)),
|
||||
(
|
||||
"artist",
|
||||
models.ForeignKey(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="artist_credit",
|
||||
to="music.artist",
|
||||
),
|
||||
),
|
||||
(
|
||||
"from_activity",
|
||||
models.ForeignKey(
|
||||
blank=True,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_NULL,
|
||||
to="federation.activity",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ["index", "credit"],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="album",
|
||||
name="artist_credit",
|
||||
field=models.ManyToManyField(
|
||||
related_name="albums",
|
||||
to="music.artistcredit",
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="track",
|
||||
name="artist_credit",
|
||||
field=models.ManyToManyField(
|
||||
related_name="tracks",
|
||||
to="music.artistcredit",
|
||||
),
|
||||
),
|
||||
migrations.RunPython(set_artist_credit, skip),
|
||||
migrations.RemoveField(
|
||||
model_name="album",
|
||||
name="artist",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="track",
|
||||
name="artist",
|
||||
),
|
||||
]
|
|
@ -24,9 +24,8 @@ from django.urls import reverse
|
|||
from django.utils import timezone
|
||||
|
||||
from funkwhale_api import musicbrainz
|
||||
from funkwhale_api.common import fields
|
||||
from funkwhale_api.common import fields, preferences, session
|
||||
from funkwhale_api.common import models as common_models
|
||||
from funkwhale_api.common import session
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
|
@ -111,7 +110,6 @@ class APIModelMixin(models.Model):
|
|||
def get_federation_id(self):
|
||||
if self.fid:
|
||||
return self.fid
|
||||
|
||||
return federation_utils.full_url(
|
||||
reverse(
|
||||
f"federation:music:{self.federation_namespace}-detail",
|
||||
|
@ -170,12 +168,12 @@ class License(models.Model):
|
|||
|
||||
class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||
def with_albums_count(self):
|
||||
return self.annotate(_albums_count=models.Count("albums"))
|
||||
return self.annotate(_albums_count=models.Count("artist_credit__albums"))
|
||||
|
||||
def with_albums(self):
|
||||
return self.prefetch_related(
|
||||
models.Prefetch(
|
||||
"albums",
|
||||
"artist_credit__albums",
|
||||
queryset=Album.objects.with_tracks_count().select_related(
|
||||
"attachment_cover", "attributed_to"
|
||||
),
|
||||
|
@ -185,7 +183,7 @@ class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
|||
def annotate_playable_by_actor(self, actor):
|
||||
tracks = (
|
||||
Upload.objects.playable_by(actor)
|
||||
.filter(track__artist=models.OuterRef("id"))
|
||||
.filter(track__artist_credit__artist=models.OuterRef("id"))
|
||||
.order_by("id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
@ -194,7 +192,9 @@ class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
|||
|
||||
def playable_by(self, actor, include=True):
|
||||
tracks = Track.objects.playable_by(actor)
|
||||
matches = self.filter(pk__in=tracks.values("artist_id")).values_list("pk")
|
||||
matches = self.filter(
|
||||
pk__in=tracks.values("artist_credit__artist")
|
||||
).values_list("pk")
|
||||
if include:
|
||||
return self.filter(pk__in=matches)
|
||||
else:
|
||||
|
@ -271,9 +271,25 @@ class Artist(APIModelMixin):
|
|||
return None
|
||||
|
||||
|
||||
def import_artist(v):
|
||||
a = Artist.get_or_create_from_api(mbid=v[0]["artist"]["id"])[0]
|
||||
return a
|
||||
def import_artist_credit(v):
|
||||
artists_credits = []
|
||||
for i, ac in enumerate(v):
|
||||
artist, create = Artist.get_or_create_from_api(mbid=ac["artist"]["id"])
|
||||
|
||||
if "joinphrase" in ac["artist"]:
|
||||
joinphrase = ac["artist"]["joinphrase"]
|
||||
elif i < len(v):
|
||||
joinphrase = preferences.get("music__default_join_phrase")
|
||||
else:
|
||||
joinphrase = ""
|
||||
artist_credit, created = ArtistCredit.objects.get_or_create(
|
||||
artist=artist,
|
||||
credit=ac["artist"]["name"],
|
||||
index=i,
|
||||
joinphrase=joinphrase,
|
||||
)
|
||||
artists_credits.append(artist_credit)
|
||||
return artists_credits
|
||||
|
||||
|
||||
def parse_date(v):
|
||||
|
@ -289,6 +305,39 @@ def import_tracks(instance, cleaned_data, raw_data):
|
|||
importers.load(Track, track_cleaned_data, track_data, Track.import_hooks)
|
||||
|
||||
|
||||
class ArtistCreditQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||
def albums(self):
|
||||
albums_ids = self.prefetch_related("albums").values_list("albums")
|
||||
return Album.objects.filter(id__in=albums_ids)
|
||||
|
||||
|
||||
class ArtistCredit(APIModelMixin):
|
||||
artist = models.ForeignKey(
|
||||
Artist, related_name="artist_credit", on_delete=models.CASCADE
|
||||
)
|
||||
credit = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=500,
|
||||
)
|
||||
joinphrase = models.CharField(
|
||||
null=True,
|
||||
blank=True,
|
||||
max_length=250,
|
||||
)
|
||||
index = models.IntegerField(
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
federation_namespace = "artistcredit"
|
||||
|
||||
objects = ArtistCreditQuerySet.as_manager()
|
||||
|
||||
class Meta:
|
||||
ordering = ["index", "credit"]
|
||||
|
||||
|
||||
class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||
def with_tracks_count(self):
|
||||
return self.annotate(_tracks_count=models.Count("tracks"))
|
||||
|
@ -296,7 +345,7 @@ class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
|||
def annotate_playable_by_actor(self, actor):
|
||||
tracks = (
|
||||
Upload.objects.playable_by(actor)
|
||||
.filter(track__album=models.OuterRef("id"))
|
||||
.filter(track__artist_credit__albums=models.OuterRef("id"))
|
||||
.order_by("id")
|
||||
.values("id")[:1]
|
||||
)
|
||||
|
@ -328,7 +377,7 @@ class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
|||
|
||||
class Album(APIModelMixin):
|
||||
title = models.TextField()
|
||||
artist = models.ForeignKey(Artist, related_name="albums", on_delete=models.CASCADE)
|
||||
artist_credit = models.ManyToManyField(ArtistCredit, related_name="albums")
|
||||
release_date = models.DateField(null=True, blank=True, db_index=True)
|
||||
release_group_id = models.UUIDField(null=True, blank=True)
|
||||
attachment_cover = models.ForeignKey(
|
||||
|
@ -379,9 +428,9 @@ class Album(APIModelMixin):
|
|||
"title": {"musicbrainz_field_name": "title"},
|
||||
"release_date": {"musicbrainz_field_name": "date", "converter": parse_date},
|
||||
"type": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()},
|
||||
"artist": {
|
||||
"artist_credit": {
|
||||
"musicbrainz_field_name": "artist-credit",
|
||||
"converter": import_artist,
|
||||
"converter": import_artist_credit,
|
||||
},
|
||||
}
|
||||
objects = AlbumQuerySet.as_manager()
|
||||
|
@ -404,6 +453,13 @@ class Album(APIModelMixin):
|
|||
kwargs.update({"title": title})
|
||||
return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
|
||||
|
||||
@property
|
||||
def get_artist_credit_string(self):
|
||||
return utils.get_artist_credit_string(self)
|
||||
|
||||
def get_artists_list(self):
|
||||
return [ac.artist for ac in self.artist_credit.all()]
|
||||
|
||||
|
||||
def import_tags(instance, cleaned_data, raw_data):
|
||||
MINIMUM_COUNT = 2
|
||||
|
@ -427,11 +483,11 @@ def import_album(v):
|
|||
class TrackQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||
def for_nested_serialization(self):
|
||||
return self.prefetch_related(
|
||||
"artist",
|
||||
"artist_credit",
|
||||
Prefetch(
|
||||
"album",
|
||||
queryset=Album.objects.select_related(
|
||||
"artist", "attachment_cover"
|
||||
queryset=Album.objects.prefetch_related(
|
||||
"artist_credit", "attachment_cover"
|
||||
).annotate(_prefetched_tracks_count=Count("tracks")),
|
||||
),
|
||||
)
|
||||
|
@ -484,7 +540,7 @@ def get_artist(release_list):
|
|||
class Track(APIModelMixin):
|
||||
mbid = models.UUIDField(db_index=True, null=True, blank=True)
|
||||
title = models.TextField()
|
||||
artist = models.ForeignKey(Artist, related_name="tracks", on_delete=models.CASCADE)
|
||||
artist_credit = models.ManyToManyField(ArtistCredit, related_name="tracks")
|
||||
disc_number = models.PositiveIntegerField(null=True, blank=True)
|
||||
position = models.PositiveIntegerField(null=True, blank=True)
|
||||
album = models.ForeignKey(
|
||||
|
@ -526,11 +582,9 @@ class Track(APIModelMixin):
|
|||
musicbrainz_mapping = {
|
||||
"mbid": {"musicbrainz_field_name": "id"},
|
||||
"title": {"musicbrainz_field_name": "title"},
|
||||
"artist": {
|
||||
"artist_credit": {
|
||||
"musicbrainz_field_name": "artist-credit",
|
||||
"converter": lambda v: Artist.get_or_create_from_api(
|
||||
mbid=v[0]["artist"]["id"]
|
||||
)[0],
|
||||
"converter": import_artist_credit,
|
||||
},
|
||||
"album": {"musicbrainz_field_name": "release-list", "converter": import_album},
|
||||
}
|
||||
|
@ -558,19 +612,29 @@ class Track(APIModelMixin):
|
|||
def get_moderation_url(self):
|
||||
return f"/manage/library/tracks/{self.pk}"
|
||||
|
||||
@property
|
||||
def get_artist_credit_string(self):
|
||||
return utils.get_artist_credit_string(self)
|
||||
|
||||
def get_artists_list(self):
|
||||
return [ac.artist for ac in self.artist_credit.all()]
|
||||
|
||||
def save(self, **kwargs):
|
||||
try:
|
||||
self.artist
|
||||
except Artist.DoesNotExist:
|
||||
self.artist = self.album.artist
|
||||
super().save(**kwargs)
|
||||
# to do :
|
||||
# try:
|
||||
# self.artist_credit.all()[0]
|
||||
# except IndexError:
|
||||
# self.artist_credit.set(self.album.artist_credit.all())
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
try:
|
||||
return f"{self.artist.name} - {self.album.title} - {self.title}"
|
||||
return (
|
||||
f"{self.get_artist_credit_string} - {self.album.title} - {self.title}"
|
||||
)
|
||||
except AttributeError:
|
||||
return f"{self.artist.name} - {self.title}"
|
||||
return f"{self.get_artist_credit_string} - {self.title}"
|
||||
|
||||
@property
|
||||
def cover(self):
|
||||
|
@ -608,33 +672,43 @@ class Track(APIModelMixin):
|
|||
if not track_data:
|
||||
raise ValueError("No track found matching this ID")
|
||||
|
||||
track_artist_mbid = None
|
||||
for ac in track_data["recording"]["artist-credit"]:
|
||||
# to do : support multiple artist_credit in get_or_create_from_release
|
||||
artists_credits = []
|
||||
for i, ac in enumerate(track_data["recording"]["artist-credit"]):
|
||||
try:
|
||||
ac_mbid = ac["artist"]["id"]
|
||||
except TypeError:
|
||||
# it's probably a string, like "feat."
|
||||
# it's probably a string, like "feat." -> why should not be a string be the id..
|
||||
continue
|
||||
|
||||
if ac_mbid == str(album.artist.mbid):
|
||||
continue
|
||||
track_artist = Artist.get_or_create_from_api(ac_mbid)[0]
|
||||
|
||||
track_artist_mbid = ac_mbid
|
||||
break
|
||||
track_artist_mbid = track_artist_mbid or album.artist.mbid
|
||||
if track_artist_mbid == str(album.artist.mbid):
|
||||
track_artist = album.artist
|
||||
else:
|
||||
track_artist = Artist.get_or_create_from_api(track_artist_mbid)[0]
|
||||
return cls.objects.update_or_create(
|
||||
if not "joinphrase" in ac:
|
||||
joinphrase = ""
|
||||
else:
|
||||
joinphrase = ac["joinphrase"]
|
||||
|
||||
artist_credit, create = ArtistCredit.objects.get_or_create(
|
||||
artist=track_artist,
|
||||
credit=ac["artist"]["name"],
|
||||
joinphrase=joinphrase,
|
||||
index=i,
|
||||
)
|
||||
artists_credits.append(artist_credit)
|
||||
|
||||
if album.artist_credit.all() != artist_credit:
|
||||
album.artist_credit.set(artists_credits)
|
||||
|
||||
track = cls.objects.update_or_create(
|
||||
mbid=mbid,
|
||||
defaults={
|
||||
"position": int(track["position"]),
|
||||
"title": track["recording"]["title"],
|
||||
"album": album,
|
||||
"artist": track_artist,
|
||||
},
|
||||
)
|
||||
track[0].artist_credit.set(artists_credits)
|
||||
return track
|
||||
|
||||
@property
|
||||
def listen_url(self) -> str:
|
||||
|
@ -804,7 +878,7 @@ class Upload(models.Model):
|
|||
title_parts.append(self.track.title)
|
||||
if self.track.album:
|
||||
title_parts.append(self.track.album.title)
|
||||
title_parts.append(self.track.artist.name)
|
||||
title_parts.append(self.track.get_artist_credit_string)
|
||||
|
||||
title = " - ".join(title_parts)
|
||||
filename = f"{title}.{extension}"
|
||||
|
@ -984,9 +1058,21 @@ class Upload(models.Model):
|
|||
if self.track.album
|
||||
else tags_models.TaggedItem.objects.none()
|
||||
)
|
||||
artist_tags = self.track.artist.tagged_items.all()
|
||||
artist_tags = [
|
||||
ac.artist.tagged_items.all() for ac in self.track.artist_credit.all()
|
||||
]
|
||||
non_empty_artist_tags = []
|
||||
for qs in artist_tags:
|
||||
if qs.exists():
|
||||
non_empty_artist_tags.append(qs)
|
||||
|
||||
items = (track_tags | album_tags | artist_tags).order_by("tag__name")
|
||||
if non_empty_artist_tags:
|
||||
final_qs = (track_tags | album_tags).union(*non_empty_artist_tags)
|
||||
else:
|
||||
final_qs = track_tags | album_tags
|
||||
# this is needed to avoid *** RuntimeError: generator raised StopIteration
|
||||
final_list = [obj for obj in final_qs]
|
||||
items = sorted(final_list, key=lambda x: x.tag.name if x.tag else "")
|
||||
return items
|
||||
|
||||
|
||||
|
|
|
@ -126,7 +126,12 @@ class TrackMutationSerializer(CoverMutation, TagMutation, DescriptionMutation):
|
|||
return serialized_relations
|
||||
|
||||
def post_apply(self, obj, validated_data):
|
||||
channel = obj.artist.get_channel()
|
||||
# to do : channel, only allow channel tracks to have one artist credit to avoid errors ?
|
||||
channel = (
|
||||
obj.artist_credit.all()[0].artist.get_channel()
|
||||
if len(obj.artist_credit.all()) == 1
|
||||
else None
|
||||
)
|
||||
if channel:
|
||||
upload = channel.library.uploads.filter(track=obj).first()
|
||||
if upload:
|
||||
|
|
|
@ -81,12 +81,12 @@ class ArtistAlbumSerializer(serializers.Serializer):
|
|||
fid = serializers.URLField()
|
||||
mbid = serializers.UUIDField()
|
||||
title = serializers.CharField()
|
||||
artist = serializers.SerializerMethodField()
|
||||
artist_credit = serializers.SerializerMethodField()
|
||||
release_date = serializers.DateField()
|
||||
creation_date = serializers.DateTimeField()
|
||||
|
||||
def get_artist(self, o) -> int:
|
||||
return o.artist_id
|
||||
def get_artist_credit(self, o) -> int:
|
||||
return [ac.id for ac in o.artist_credit.all()]
|
||||
|
||||
def get_tracks_count(self, o) -> int:
|
||||
return len(o.tracks.all())
|
||||
|
@ -113,7 +113,7 @@ class ArtistWithAlbumsInlineChannelSerializer(serializers.Serializer):
|
|||
|
||||
|
||||
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
albums = ArtistAlbumSerializer(many=True)
|
||||
albums = serializers.SerializerMethodField()
|
||||
tags = serializers.SerializerMethodField()
|
||||
attributed_to = APIActorSerializer(allow_null=True)
|
||||
channel = ArtistWithAlbumsInlineChannelSerializer(allow_null=True)
|
||||
|
@ -127,6 +127,10 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize
|
|||
is_local = serializers.BooleanField()
|
||||
cover = CoverField(allow_null=True)
|
||||
|
||||
def get_albums(self, artist):
|
||||
albums = artist.artist_credit.albums()
|
||||
return ArtistAlbumSerializer(albums, many=True).data
|
||||
|
||||
@extend_schema_field({"type": "array", "items": {"type": "string"}})
|
||||
def get_tags(self, obj):
|
||||
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
||||
|
@ -159,8 +163,16 @@ class SimpleArtistSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
|
||||
|
||||
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
class ArtistCreditSerializer(serializers.ModelSerializer):
|
||||
artist = SimpleArtistSerializer()
|
||||
|
||||
class Meta:
|
||||
model = models.ArtistCredit
|
||||
fields = ["artist", "credit", "joinphrase", "index"]
|
||||
|
||||
|
||||
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
artist_credit = ArtistCreditSerializer(many=True)
|
||||
cover = CoverField(allow_null=True)
|
||||
is_playable = serializers.SerializerMethodField()
|
||||
tags = serializers.SerializerMethodField()
|
||||
|
@ -203,7 +215,7 @@ class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
|||
|
||||
|
||||
class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||
artist = SimpleArtistSerializer()
|
||||
artist_credit = ArtistCreditSerializer(many=True)
|
||||
cover = CoverField(allow_null=True)
|
||||
tracks_count = serializers.SerializerMethodField()
|
||||
|
||||
|
@ -217,7 +229,7 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
|
|||
"fid",
|
||||
"mbid",
|
||||
"title",
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"release_date",
|
||||
"cover",
|
||||
"creation_date",
|
||||
|
@ -257,7 +269,7 @@ def sort_uploads_for_listen(uploads):
|
|||
|
||||
|
||||
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||
artist = SimpleArtistSerializer()
|
||||
artist_credit = ArtistCreditSerializer(many=True)
|
||||
album = TrackAlbumSerializer(read_only=True)
|
||||
uploads = serializers.SerializerMethodField()
|
||||
listen_url = serializers.SerializerMethodField()
|
||||
|
@ -400,9 +412,9 @@ class UploadSerializer(serializers.ModelSerializer):
|
|||
|
||||
def filter_album(qs, context):
|
||||
if "channel" in context:
|
||||
return qs.filter(artist__channel=context["channel"])
|
||||
return qs.filter(artist_credit__artist__channel=context["channel"])
|
||||
if "actor" in context:
|
||||
return qs.filter(artist__attributed_to=context["actor"])
|
||||
return qs.filter(artist_credit__artist__attributed_to=context["actor"])
|
||||
|
||||
return qs.none()
|
||||
|
||||
|
@ -567,12 +579,12 @@ class SimpleAlbumSerializer(serializers.ModelSerializer):
|
|||
class TrackActivitySerializer(activity_serializers.ModelSerializer):
|
||||
type = serializers.SerializerMethodField()
|
||||
name = serializers.CharField(source="title")
|
||||
artist = serializers.CharField(source="artist.name")
|
||||
artist_credit = serializers.CharField(source="get_artist_credit_string")
|
||||
album = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = models.Track
|
||||
fields = ["id", "local_id", "name", "type", "artist", "album"]
|
||||
fields = ["id", "local_id", "name", "type", "artist_credit", "album"]
|
||||
|
||||
def get_type(self, obj):
|
||||
return "Audio"
|
||||
|
@ -612,9 +624,9 @@ class OembedSerializer(serializers.Serializer):
|
|||
embed_id = None
|
||||
embed_type = None
|
||||
if match.url_name == "library_track":
|
||||
qs = models.Track.objects.select_related("artist", "album__artist").filter(
|
||||
pk=int(match.kwargs["pk"])
|
||||
)
|
||||
qs = models.Track.objects.prefetch_related(
|
||||
"artist_credit", "album__artist_credit"
|
||||
).filter(pk=int(match.kwargs["pk"]))
|
||||
try:
|
||||
track = qs.get()
|
||||
except models.Track.DoesNotExist:
|
||||
|
@ -623,7 +635,7 @@ class OembedSerializer(serializers.Serializer):
|
|||
)
|
||||
embed_type = "track"
|
||||
embed_id = track.pk
|
||||
data["title"] = f"{track.title} by {track.artist.name}"
|
||||
data["title"] = f"{track.title} by {track.get_artist_credit_string}"
|
||||
if track.attachment_cover:
|
||||
data[
|
||||
"thumbnail_url"
|
||||
|
@ -637,15 +649,17 @@ class OembedSerializer(serializers.Serializer):
|
|||
data["thumbnail_width"] = 200
|
||||
data["thumbnail_height"] = 200
|
||||
data["description"] = track.full_name
|
||||
data["author_name"] = track.artist.name
|
||||
data["author_name"] = track.get_artist_credit_string
|
||||
data["height"] = 150
|
||||
# here we take the first artist since oembed standart do not allow a list of url
|
||||
data["author_url"] = federation_utils.full_url(
|
||||
common_utils.spa_reverse(
|
||||
"library_artist", kwargs={"pk": track.artist.pk}
|
||||
"library_artist",
|
||||
kwargs={"pk": track.artist_credit.all()[0].artist.pk},
|
||||
)
|
||||
)
|
||||
elif match.url_name == "library_album":
|
||||
qs = models.Album.objects.select_related("artist").filter(
|
||||
qs = models.Album.objects.prefetch_related("artist_credit").filter(
|
||||
pk=int(match.kwargs["pk"])
|
||||
)
|
||||
try:
|
||||
|
@ -662,15 +676,17 @@ class OembedSerializer(serializers.Serializer):
|
|||
] = album.attachment_cover.download_url_medium_square_crop
|
||||
data["thumbnail_width"] = 200
|
||||
data["thumbnail_height"] = 200
|
||||
data["title"] = f"{album.title} by {album.artist.name}"
|
||||
data["description"] = f"{album.title} by {album.artist.name}"
|
||||
data["author_name"] = album.artist.name
|
||||
data["title"] = f"{album.title} by {album.get_artist_credit_string}"
|
||||
data["description"] = f"{album.title} by {album.get_artist_credit_string}"
|
||||
data["author_name"] = album.get_artist_credit_string
|
||||
data["height"] = 400
|
||||
data["author_url"] = federation_utils.full_url(
|
||||
common_utils.spa_reverse(
|
||||
"library_artist", kwargs={"pk": album.artist.pk}
|
||||
"library_artist",
|
||||
kwargs={"pk": album.artist_credit.all()[0].artist.pk},
|
||||
)
|
||||
)
|
||||
|
||||
elif match.url_name == "library_artist":
|
||||
qs = models.Artist.objects.filter(pk=int(match.kwargs["pk"]))
|
||||
try:
|
||||
|
@ -681,7 +697,17 @@ class OembedSerializer(serializers.Serializer):
|
|||
)
|
||||
embed_type = "artist"
|
||||
embed_id = artist.pk
|
||||
album = artist.albums.exclude(attachment_cover=None).order_by("-id").first()
|
||||
album_ids = (
|
||||
artist.artist_credit.all()
|
||||
.prefetch_related("albums")
|
||||
.values_list("albums", flat=True)
|
||||
)
|
||||
album = (
|
||||
models.Album.objects.exclude(attachment_cover=None)
|
||||
.filter(pk__in=album_ids)
|
||||
.order_by("-id")
|
||||
.first()
|
||||
)
|
||||
|
||||
if album and album.attachment_cover:
|
||||
data[
|
||||
|
@ -791,17 +817,16 @@ class AlbumCreateSerializer(serializers.Serializer):
|
|||
release_date = serializers.DateField(required=False, allow_null=True)
|
||||
tags = tags_serializers.TagsListField(required=False)
|
||||
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
||||
|
||||
artist = common_serializers.RelatedField(
|
||||
artist_credit = common_serializers.RelatedField(
|
||||
"id",
|
||||
queryset=models.Artist.objects.exclude(channel__isnull=True),
|
||||
queryset=models.ArtistCredit.objects.exclude(artist__channel__isnull=True),
|
||||
required=True,
|
||||
serializer=None,
|
||||
filters=lambda context: {"attributed_to": context["user"].actor},
|
||||
)
|
||||
|
||||
def validate(self, validated_data):
|
||||
duplicates = validated_data["artist"].albums.filter(
|
||||
duplicates = validated_data["artist_credit"].albums.filter(
|
||||
title__iexact=validated_data["title"]
|
||||
)
|
||||
if duplicates.exists():
|
||||
|
|
|
@ -24,7 +24,9 @@ def get_twitter_card_metas(type, id):
|
|||
|
||||
|
||||
def library_track(request, pk, redirect_to_ap):
|
||||
queryset = models.Track.objects.filter(pk=pk).select_related("album", "artist")
|
||||
queryset = models.Track.objects.filter(pk=pk).prefetch_related(
|
||||
"album", "artist_credit__artist"
|
||||
)
|
||||
try:
|
||||
obj = queryset.get()
|
||||
except models.Track.DoesNotExist:
|
||||
|
@ -47,15 +49,19 @@ def library_track(request, pk, redirect_to_ap):
|
|||
{"tag": "meta", "property": "og:type", "content": "music.song"},
|
||||
{"tag": "meta", "property": "music:album:disc", "content": obj.disc_number},
|
||||
{"tag": "meta", "property": "music:album:track", "content": obj.position},
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": obj.artist.pk}),
|
||||
),
|
||||
},
|
||||
]
|
||||
# following https://ogp.me/#array
|
||||
for ac in obj.artist_credit.all():
|
||||
metas.append(
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": ac.artist.pk}),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if obj.album:
|
||||
metas.append(
|
||||
|
@ -119,7 +125,7 @@ def library_track(request, pk, redirect_to_ap):
|
|||
|
||||
|
||||
def library_album(request, pk, redirect_to_ap):
|
||||
queryset = models.Album.objects.filter(pk=pk).select_related("artist")
|
||||
queryset = models.Album.objects.filter(pk=pk).prefetch_related("artist_credit")
|
||||
try:
|
||||
obj = queryset.get()
|
||||
except models.Album.DoesNotExist:
|
||||
|
@ -136,16 +142,20 @@ def library_album(request, pk, redirect_to_ap):
|
|||
{"tag": "meta", "property": "og:url", "content": album_url},
|
||||
{"tag": "meta", "property": "og:title", "content": obj.title},
|
||||
{"tag": "meta", "property": "og:type", "content": "music.album"},
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": obj.artist.pk}),
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
# following https://ogp.me/#array
|
||||
for ac in obj.artist_credit.all():
|
||||
metas.append(
|
||||
{
|
||||
"tag": "meta",
|
||||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": ac.artist.pk}),
|
||||
),
|
||||
}
|
||||
)
|
||||
if obj.release_date:
|
||||
metas.append(
|
||||
{
|
||||
|
@ -206,7 +216,10 @@ def library_artist(request, pk, redirect_to_ap):
|
|||
)
|
||||
# we use latest album's cover as artist image
|
||||
latest_album = (
|
||||
obj.albums.exclude(attachment_cover=None).order_by("release_date").last()
|
||||
obj.artist_credit.albums()
|
||||
.exclude(attachment_cover=None)
|
||||
.order_by("release_date")
|
||||
.last()
|
||||
)
|
||||
metas = [
|
||||
{"tag": "meta", "property": "og:url", "content": artist_url},
|
||||
|
@ -234,7 +247,10 @@ def library_artist(request, pk, redirect_to_ap):
|
|||
)
|
||||
|
||||
if (
|
||||
models.Upload.objects.filter(Q(track__artist=obj) | Q(track__album__artist=obj))
|
||||
models.Upload.objects.filter(
|
||||
Q(track__artist_credit__artist=obj)
|
||||
| Q(track__album__artist_credit__artist=obj)
|
||||
)
|
||||
.playable_by(None)
|
||||
.exists()
|
||||
):
|
||||
|
|
|
@ -2,6 +2,7 @@ import collections
|
|||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
|
@ -16,7 +17,9 @@ from funkwhale_api import musicbrainz
|
|||
from funkwhale_api.common import channels, preferences
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
from funkwhale_api.federation import library as lb
|
||||
from funkwhale_api.federation import routes
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import actors, routes
|
||||
from funkwhale_api.federation import tasks as federation_tasks
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.music.management.commands import import_files
|
||||
from funkwhale_api.tags import models as tags_models
|
||||
|
@ -395,32 +398,46 @@ def federation_audio_track_to_metadata(payload, references):
|
|||
"cover_data": get_cover(payload["album"], "image"),
|
||||
"release_date": payload["album"].get("released"),
|
||||
"tags": [t["name"] for t in payload["album"].get("tags", []) or []],
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"fid": a["id"],
|
||||
"name": a["name"],
|
||||
"fdate": a["published"],
|
||||
"cover_data": get_cover(a, "image"),
|
||||
"description": a.get("description"),
|
||||
"attributed_to": references.get(a.get("attributedTo")),
|
||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
||||
"artist": {
|
||||
"fid": a["artist"]["id"],
|
||||
"name": a["artist"]["name"],
|
||||
"fdate": a["artist"]["published"],
|
||||
"cover_data": get_cover(a["artist"], "image"),
|
||||
"description": a["artist"].get("description"),
|
||||
"attributed_to": references.get(
|
||||
a["artist"].get("attributedTo")
|
||||
),
|
||||
"mbid": str(a["artist"]["musicbrainzId"])
|
||||
if a["artist"].get("musicbrainzId")
|
||||
else None,
|
||||
"tags": [t["name"] for t in a["artist"].get("tags", []) or []],
|
||||
},
|
||||
"joinphrase": (a["joinphrase"] if "joinphrase" in a else ""),
|
||||
"credit": (a["credit"] if "credit" in a else a["name"]),
|
||||
}
|
||||
for a in payload["album"]["artists"]
|
||||
for a in payload["album"]["artist_credit"]
|
||||
],
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"fid": a["id"],
|
||||
"name": a["name"],
|
||||
"fdate": a["published"],
|
||||
"description": a.get("description"),
|
||||
"attributed_to": references.get(a.get("attributedTo")),
|
||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
||||
"cover_data": get_cover(a, "image"),
|
||||
"artist": {
|
||||
"fid": a["artist"]["id"],
|
||||
"name": a["artist"]["name"],
|
||||
"fdate": a["artist"]["published"],
|
||||
"description": a["artist"].get("description"),
|
||||
"attributed_to": references.get(a["artist"].get("attributedTo")),
|
||||
"mbid": str(a["artist"]["musicbrainzId"])
|
||||
if a["artist"].get("musicbrainzId")
|
||||
else None,
|
||||
"tags": [t["name"] for t in a["artist"].get("tags", []) or []],
|
||||
"cover_data": get_cover(a["artist"], "image"),
|
||||
},
|
||||
"joinphrase": (a["joinphrase"] if "joinphrase" in a else ""),
|
||||
"credit": (a["credit"] if "credit" in a else a["name"]),
|
||||
}
|
||||
for a in payload["artists"]
|
||||
for a in payload["artist_credit"]
|
||||
],
|
||||
# federation
|
||||
"fid": payload["id"],
|
||||
|
@ -434,6 +451,7 @@ def get_owned_duplicates(upload, track):
|
|||
"""
|
||||
Ensure we skip duplicate tracks to avoid wasting user/instance storage
|
||||
"""
|
||||
|
||||
owned_libraries = upload.library.actor.libraries.all()
|
||||
return (
|
||||
models.Upload.objects.filter(
|
||||
|
@ -547,64 +565,78 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
except IndexError:
|
||||
pass
|
||||
|
||||
# get / create artist and album artist
|
||||
artists = getter(data, "artists", default=[])
|
||||
# get / create artist, artist_credit and album artist, album artist_credit
|
||||
album_artists_credits = None
|
||||
artists_data = getter(data, "artists", default=[])
|
||||
# to do : allow artist credit forced values
|
||||
if "artist" in forced_values:
|
||||
artist = forced_values["artist"]
|
||||
else:
|
||||
artist_data = artists[0]
|
||||
artist = get_artist(
|
||||
artist_data, attributed_to=attributed_to, from_activity_id=from_activity_id
|
||||
query = Q(artist=artist)
|
||||
defaults = {
|
||||
"artist": artist,
|
||||
"joinphrase": "",
|
||||
"credit": artist.name,
|
||||
}
|
||||
track_artist_credit, created = get_best_candidate_or_create(
|
||||
models.ArtistCredit, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
)
|
||||
artist_name = artist.name
|
||||
track_artists_credits = [track_artist_credit]
|
||||
else:
|
||||
if data.get("musicbrainz_id", None) or data.get("mbid", None):
|
||||
track_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||
"recording",
|
||||
data.get("musicbrainz_id"),
|
||||
attributed_to=attributed_to,
|
||||
from_activity_id=from_activity_id,
|
||||
)
|
||||
album_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||
"release",
|
||||
data.get("musicbrainz_id"),
|
||||
attributed_to=attributed_to,
|
||||
from_activity_id=from_activity_id,
|
||||
)
|
||||
else:
|
||||
track_artists_credits = get_or_create_artists_credits_from_artist_metadata(
|
||||
artists_data,
|
||||
attributed_to=attributed_to,
|
||||
from_activity_id=from_activity_id,
|
||||
)
|
||||
|
||||
if "album" in forced_values:
|
||||
album = forced_values["album"]
|
||||
album_artists_credits = track_artists_credits
|
||||
else:
|
||||
if "artist" in forced_values:
|
||||
album_artist = forced_values["artist"]
|
||||
else:
|
||||
album_artists = getter(data, "album", "artists", default=artists) or artists
|
||||
album_artist_data = album_artists[0]
|
||||
album_artist_name = album_artist_data.get("name")
|
||||
if album_artist_name == artist_name:
|
||||
album_artist = artist
|
||||
else:
|
||||
query = Q(name__iexact=album_artist_name)
|
||||
album_artist_mbid = album_artist_data.get("mbid", None)
|
||||
album_artist_fid = album_artist_data.get("fid", None)
|
||||
if album_artist_mbid:
|
||||
query |= Q(mbid=album_artist_mbid)
|
||||
if album_artist_fid:
|
||||
query |= Q(fid=album_artist_fid)
|
||||
defaults = {
|
||||
"name": album_artist_name,
|
||||
"mbid": album_artist_mbid,
|
||||
"fid": album_artist_fid,
|
||||
"from_activity_id": from_activity_id,
|
||||
"attributed_to": album_artist_data.get(
|
||||
"attributed_to", attributed_to
|
||||
),
|
||||
}
|
||||
if album_artist_data.get("fdate"):
|
||||
defaults["creation_date"] = album_artist_data.get("fdate")
|
||||
if album_artists_credits:
|
||||
pass
|
||||
elif data.get("musicbrainz_albumid", None):
|
||||
try:
|
||||
album_artists_credits = (
|
||||
get_or_create_artists_credits_from_musicbrainz(
|
||||
"release",
|
||||
data.get("musicbrainz_albumid"),
|
||||
attributed_to=attributed_to,
|
||||
from_activity_id=from_activity_id,
|
||||
)
|
||||
)
|
||||
except ResponseError as e:
|
||||
logger.error(
|
||||
f"Couldn't get Musicbrainz information for track with {track_mbid} mbid \
|
||||
because of the following exeption : {e}. Plz try again later."
|
||||
)
|
||||
|
||||
album_artist, created = get_best_candidate_or_create(
|
||||
models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
elif album_artists := getter(data, "album", "artists", default=None):
|
||||
album_artists_credits = (
|
||||
get_or_create_artists_credits_from_artist_metadata(
|
||||
album_artists,
|
||||
attributed_to=attributed_to,
|
||||
from_activity_id=from_activity_id,
|
||||
)
|
||||
)
|
||||
if created:
|
||||
tags_models.add_tags(
|
||||
album_artist, *album_artist_data.get("tags", [])
|
||||
)
|
||||
common_utils.attach_content(
|
||||
album_artist,
|
||||
"description",
|
||||
album_artist_data.get("description"),
|
||||
)
|
||||
common_utils.attach_file(
|
||||
album_artist,
|
||||
"attachment_cover",
|
||||
album_artist_data.get("cover_data"),
|
||||
)
|
||||
else:
|
||||
album_artists_credits = track_artists_credits
|
||||
|
||||
# get / create album
|
||||
if "album" in data:
|
||||
|
@ -615,13 +647,15 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
if album_mbid:
|
||||
query = Q(mbid=album_mbid)
|
||||
else:
|
||||
query = Q(title__iexact=album_title, artist=album_artist)
|
||||
query = Q(
|
||||
title__iexact=album_title, artist_credit__in=album_artists_credits
|
||||
)
|
||||
|
||||
if album_fid:
|
||||
query |= Q(fid=album_fid)
|
||||
|
||||
defaults = {
|
||||
"title": album_title,
|
||||
"artist": album_artist,
|
||||
"mbid": album_mbid,
|
||||
"release_date": album_data.get("release_date"),
|
||||
"fid": album_fid,
|
||||
|
@ -634,6 +668,8 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
album, created = get_best_candidate_or_create(
|
||||
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
)
|
||||
album.artist_credit.set(album_artists_credits)
|
||||
|
||||
if created:
|
||||
tags_models.add_tags(album, *album_data.get("tags", []))
|
||||
common_utils.attach_content(
|
||||
|
@ -677,7 +713,7 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
|
||||
query = Q(
|
||||
title__iexact=track_title,
|
||||
artist=artist,
|
||||
artist_credit__in=track_artists_credits,
|
||||
album=album,
|
||||
position=position,
|
||||
disc_number=disc_number,
|
||||
|
@ -690,17 +726,10 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
if track_fid:
|
||||
query |= Q(fid=track_fid)
|
||||
|
||||
if album and len(artists) > 1:
|
||||
# we use the second artist to preserve featuring information
|
||||
artist = artist = get_artist(
|
||||
artists[1], attributed_to=attributed_to, from_activity_id=from_activity_id
|
||||
)
|
||||
|
||||
defaults = {
|
||||
"title": track_title,
|
||||
"album": album,
|
||||
"mbid": track_mbid,
|
||||
"artist": artist,
|
||||
"position": position,
|
||||
"disc_number": disc_number,
|
||||
"fid": track_fid,
|
||||
|
@ -723,7 +752,7 @@ def _get_track(data, attributed_to=None, **forced_values):
|
|||
tags_models.add_tags(track, *tags)
|
||||
common_utils.attach_content(track, "description", description)
|
||||
common_utils.attach_file(track, "attachment_cover", cover_data)
|
||||
|
||||
track.artist_credit.set(track_artists_credits)
|
||||
return track
|
||||
|
||||
|
||||
|
@ -731,6 +760,7 @@ def get_artist(artist_data, attributed_to, from_activity_id):
|
|||
artist_mbid = artist_data.get("mbid", None)
|
||||
artist_fid = artist_data.get("fid", None)
|
||||
artist_name = artist_data["name"]
|
||||
creation_date = artist_data.get("fdate", timezone.now())
|
||||
|
||||
if artist_mbid:
|
||||
query = Q(mbid=artist_mbid)
|
||||
|
@ -738,12 +768,14 @@ def get_artist(artist_data, attributed_to, from_activity_id):
|
|||
query = Q(name__iexact=artist_name)
|
||||
if artist_fid:
|
||||
query |= Q(fid=artist_fid)
|
||||
|
||||
defaults = {
|
||||
"name": artist_name,
|
||||
"mbid": artist_mbid,
|
||||
"fid": artist_fid,
|
||||
"from_activity_id": from_activity_id,
|
||||
"attributed_to": artist_data.get("attributed_to", attributed_to),
|
||||
"creation_date": creation_date,
|
||||
}
|
||||
if artist_data.get("fdate"):
|
||||
defaults["creation_date"] = artist_data.get("fdate")
|
||||
|
@ -762,6 +794,210 @@ def get_artist(artist_data, attributed_to, from_activity_id):
|
|||
return artist
|
||||
|
||||
|
||||
def get_or_create_artists_credits_from_musicbrainz(
|
||||
mb_obj_type, track_mbid, attributed_to, from_activity_id
|
||||
):
|
||||
try:
|
||||
if mb_obj_type == "release":
|
||||
mb_obj = musicbrainz.api.releases.get(track_mbid, includes=["artists"])
|
||||
elif mb_obj_type == "recording":
|
||||
mb_obj = musicbrainz.api.recordings.get(track_mbid, includes=["artists"])
|
||||
except ResponseError as e:
|
||||
logger.error(
|
||||
f"Couldn't get Musicbrainz information for track with {track_mbid} mbid \
|
||||
because of the following exeption : {e}"
|
||||
)
|
||||
raise
|
||||
|
||||
artists_credits = []
|
||||
artists_credits_data = []
|
||||
for i, ac in enumerate(mb_obj["artist-credit"]):
|
||||
artist_mbid = ac["artist"]["id"]
|
||||
artist_name = ac["artist"]["name"]
|
||||
joinphrase = ac["joinphrase"]
|
||||
credit = ac["name"]
|
||||
|
||||
# artist creation
|
||||
query = Q(mbid=artist_mbid)
|
||||
|
||||
defaults = {
|
||||
"name": artist_name,
|
||||
"mbid": artist_mbid,
|
||||
"from_activity_id": from_activity_id,
|
||||
"attributed_to": attributed_to,
|
||||
}
|
||||
|
||||
artist, created = get_best_candidate_or_create(
|
||||
models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
)
|
||||
|
||||
# we could import tag, description, cover here.
|
||||
|
||||
# artist_credit creation
|
||||
defaults = {
|
||||
"artist": artist,
|
||||
"joinphrase": joinphrase,
|
||||
"credit": credit,
|
||||
"index": i,
|
||||
}
|
||||
query = (
|
||||
Q(artist=artist.pk)
|
||||
& Q(joinphrase=joinphrase)
|
||||
& Q(credit=credit)
|
||||
& Q(index=i)
|
||||
)
|
||||
|
||||
artist_credit, created = get_best_candidate_or_create(
|
||||
models.ArtistCredit, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
)
|
||||
artists_credits.append(artist_credit)
|
||||
|
||||
return artists_credits
|
||||
|
||||
|
||||
def parse_credits(artist_string, forced_joinphrase, forced_index, forced_artist=None):
|
||||
"""
|
||||
Return a list of parsed artist_credit information from a string like :
|
||||
LoveDiversity featuring Hatingprisons
|
||||
"""
|
||||
if not artist_string:
|
||||
return []
|
||||
join_phrase = preferences.get("music__join_phrases")
|
||||
join_phrase_regex = re.compile(rf"({join_phrase})", re.IGNORECASE)
|
||||
split = re.split(join_phrase_regex, artist_string)
|
||||
raw_artists_credits = tuple(zip(split[0::2], split[1::2]))
|
||||
|
||||
artists_credits_tuple = []
|
||||
for index, raw_artist_credit in enumerate(raw_artists_credits):
|
||||
clean_credit = raw_artist_credit[0].strip()
|
||||
clean_join_phrase = raw_artist_credit[1]
|
||||
if clean_join_phrase == "( ":
|
||||
clean_join_phrase = "("
|
||||
if clean_join_phrase == ") ":
|
||||
clean_join_phrase = ")"
|
||||
if forced_joinphrase:
|
||||
clean_join_phrase = forced_joinphrase
|
||||
|
||||
artists_credits_tuple.append(
|
||||
(
|
||||
clean_credit,
|
||||
clean_join_phrase,
|
||||
(index if not forced_index else forced_index),
|
||||
forced_artist,
|
||||
)
|
||||
)
|
||||
|
||||
# impar split :
|
||||
if len(split) % 2 != 0 and split[len(split) - 1] != "" and len(split) > 1:
|
||||
artists_credits_tuple.append(
|
||||
(
|
||||
str(split[len(split) - 1]).rstrip(),
|
||||
("" if not forced_joinphrase else forced_joinphrase),
|
||||
(len(artists_credits_tuple) if not forced_index else forced_index),
|
||||
forced_artist,
|
||||
)
|
||||
)
|
||||
|
||||
# if "name" is empty or didn't split
|
||||
if not raw_artists_credits:
|
||||
clean_credit = forced_artist.name if forced_artist else artist_string
|
||||
artists_credits_tuple.append(
|
||||
(
|
||||
clean_credit,
|
||||
("" if not forced_joinphrase else forced_joinphrase),
|
||||
(0 if not forced_index else forced_index),
|
||||
forced_artist,
|
||||
)
|
||||
)
|
||||
return artists_credits_tuple
|
||||
|
||||
|
||||
def get_or_create_artists_credits_from_artist_metadata(
|
||||
artists_data, attributed_to, from_activity_id
|
||||
):
|
||||
artists_credits = []
|
||||
raw_artists_credits = []
|
||||
artist = None
|
||||
for i, artist_data in enumerate(artists_data):
|
||||
if i + 1 == len(artists_data):
|
||||
joinphrase = ""
|
||||
elif len(artists_data) > 1:
|
||||
joinphrase = preferences.get("music__default_join_phrase")
|
||||
else:
|
||||
joinphrase = None
|
||||
|
||||
artist = get_artist(artist_data, attributed_to, from_activity_id)
|
||||
|
||||
raw_artists_credits.extend(
|
||||
parse_credits(
|
||||
(
|
||||
artist.name
|
||||
if artist
|
||||
else artist_data.get("names", artist_data.get("name"))
|
||||
),
|
||||
joinphrase,
|
||||
i,
|
||||
artist,
|
||||
)
|
||||
)
|
||||
for parsed_artist_credit in raw_artists_credits:
|
||||
artist_obj = parsed_artist_credit[3]
|
||||
defaults = {
|
||||
"artist": artist_obj,
|
||||
"credit": parsed_artist_credit[0],
|
||||
"joinphrase": parsed_artist_credit[1],
|
||||
"index": parsed_artist_credit[2],
|
||||
}
|
||||
query = (
|
||||
Q(artist=artist_obj)
|
||||
& Q(credit=parsed_artist_credit[0])
|
||||
& Q(joinphrase=parsed_artist_credit[1])
|
||||
& Q(index=parsed_artist_credit[2])
|
||||
)
|
||||
artist_credit, created = get_best_candidate_or_create(
|
||||
models.ArtistCredit, query, defaults, ["artist", "joinphrase"]
|
||||
)
|
||||
artists_credits.append(artist_credit)
|
||||
|
||||
return artists_credits
|
||||
|
||||
|
||||
def get_or_create_artists_credits_from_artist_credit_metadata(
|
||||
artists_credits_data, attributed_to, from_activity_id
|
||||
):
|
||||
artists_credits = []
|
||||
for i, ac in enumerate(artists_credits_data):
|
||||
if i + 1 == len(artists_credits_data):
|
||||
joinphrase = ""
|
||||
elif "joinphrase" in ac:
|
||||
joinphrase = ac["joinphrase"]
|
||||
else:
|
||||
joinphrase = ", "
|
||||
|
||||
artist_lookup = {"name": ac["artist"]["name"]}
|
||||
if "mbid" in ac:
|
||||
artist_lookup["mbid"] = ac["mbid"]
|
||||
|
||||
credit = ac.get("credit", ac["artist"]["name"])
|
||||
artist_obj = get_artist(artist_lookup, attributed_to, from_activity_id)
|
||||
defaults = {
|
||||
"artist": artist_obj,
|
||||
"credit": credit,
|
||||
"joinphrase": joinphrase,
|
||||
"index": 0,
|
||||
}
|
||||
query = Q(credit=credit) & Q(joinphrase=joinphrase) & Q(index=0)
|
||||
|
||||
if "mbid" in ac:
|
||||
query &= Q(artist__mbid=ac["mbid"])
|
||||
artist_credit, created = get_best_candidate_or_create(
|
||||
models.ArtistCredit, query, defaults, ["artist", "joinphrase"]
|
||||
)
|
||||
artists_credits.append(artist_credit)
|
||||
|
||||
return artists_credits
|
||||
|
||||
|
||||
@receiver(signals.upload_import_status_updated)
|
||||
def broadcast_import_status_update_to_owner(old_status, new_status, upload, **kwargs):
|
||||
user = upload.library.actor.get_user()
|
||||
|
@ -883,7 +1119,7 @@ def get_prunable_albums():
|
|||
|
||||
|
||||
def get_prunable_artists():
|
||||
return models.Artist.objects.filter(tracks__isnull=True, albums__isnull=True)
|
||||
return models.Artist.objects.filter(artist_credit__isnull=True)
|
||||
|
||||
|
||||
def update_library_entity(obj, data):
|
||||
|
@ -914,8 +1150,8 @@ UPDATE_CONFIG = {
|
|||
)
|
||||
},
|
||||
},
|
||||
"artists": {},
|
||||
"album": {"title": {}, "mbid": {}, "release_date": {}},
|
||||
"artist": {"name": {}, "mbid": {}},
|
||||
"album_artist": {"name": {}, "mbid": {}},
|
||||
}
|
||||
|
||||
|
@ -929,11 +1165,15 @@ def update_track_metadata(audio_metadata, track):
|
|||
to_update = [
|
||||
("track", track, lambda data: data),
|
||||
("album", track.album, lambda data: data["album"]),
|
||||
("artist", track.artist, lambda data: data["artists"][0]),
|
||||
(
|
||||
"artists",
|
||||
track.artist_credit.all(),
|
||||
lambda data: data["artists"],
|
||||
),
|
||||
(
|
||||
"album_artist",
|
||||
track.album.artist if track.album else None,
|
||||
lambda data: data["album"]["artists"][0],
|
||||
track.album.artist_credit.all() if track.album else None,
|
||||
lambda data: data["album"]["artists"],
|
||||
),
|
||||
]
|
||||
for id, obj, data_getter in to_update:
|
||||
|
@ -944,6 +1184,60 @@ def update_track_metadata(audio_metadata, track):
|
|||
obj_data = data_getter(new_data)
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
if id == "artists":
|
||||
if new_data.get("mbid", False):
|
||||
logger.warning(
|
||||
"If a track mbid is provided, it will be use to generate artist_credit \
|
||||
information. If you want to set a custom artist_credit you nee to remove the track mbid"
|
||||
)
|
||||
track_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||
"recording", new_data.get("mbid"), None, None
|
||||
)
|
||||
else:
|
||||
track_artists_credits = (
|
||||
get_or_create_artists_credits_from_artist_credit_metadata(
|
||||
[
|
||||
{"artist": o, "joinphrase": o["joinphrase"]}
|
||||
for o in obj_data
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
)
|
||||
if track_artists_credits == obj:
|
||||
continue
|
||||
|
||||
track.artist_credit.set(track_artists_credits)
|
||||
continue
|
||||
|
||||
if id == "album_artist":
|
||||
if new_data["album"].get("mbid", False):
|
||||
logger.warning(
|
||||
"If a album mbid is provided, it will be use to generate album artist_credit \
|
||||
information. If you want to set a custom artist_credit you nee to remove the track mbid"
|
||||
)
|
||||
album_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||
"release", new_data["album"].get("mbid"), None, None
|
||||
)
|
||||
else:
|
||||
album_artists_credits = (
|
||||
get_or_create_artists_credits_from_artist_credit_metadata(
|
||||
[
|
||||
{"artist": o, "joinphrase": o["joinphrase"]}
|
||||
for o in obj_data
|
||||
],
|
||||
None,
|
||||
None,
|
||||
)
|
||||
)
|
||||
|
||||
if album_artists_credits == obj:
|
||||
continue
|
||||
|
||||
track.album.artist_credit.set(album_artists_credits)
|
||||
continue
|
||||
|
||||
for field, config in UPDATE_CONFIG[id].items():
|
||||
getter = config.get(
|
||||
"getter", lambda data, field: data[config.get("field", field)]
|
||||
|
@ -960,7 +1254,6 @@ def update_track_metadata(audio_metadata, track):
|
|||
|
||||
if obj_updated_fields:
|
||||
obj.save(update_fields=obj_updated_fields)
|
||||
|
||||
tags_models.set_tags(track, *new_data.get("tags", []))
|
||||
|
||||
if track.album and "album" in new_data and new_data["album"].get("cover_data"):
|
||||
|
|
|
@ -151,3 +151,11 @@ def browse_dir(root, path):
|
|||
files.append({"name": el, "dir": False})
|
||||
|
||||
return dirs + files
|
||||
|
||||
|
||||
def get_artist_credit_string(obj):
|
||||
# to do : index ?
|
||||
final_credit = ""
|
||||
for ac in obj.artist_credit.all():
|
||||
final_credit = final_credit + ac.credit + ac.joinphrase
|
||||
return final_credit
|
||||
|
|
|
@ -4,11 +4,13 @@ import logging
|
|||
import urllib.parse
|
||||
|
||||
import django.db.utils
|
||||
|
||||
import requests.exceptions
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.db import transaction
|
||||
from django.db.models import Count, F, Prefetch, Q, Sum
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils import timezone
|
||||
from drf_spectacular.utils import OpenApiParameter, extend_schema, extend_schema_view
|
||||
from rest_framework import mixins, renderers
|
||||
|
@ -123,7 +125,7 @@ class ArtistViewSet(
|
|||
.prefetch_related(
|
||||
"channel__actor",
|
||||
Prefetch(
|
||||
"tracks",
|
||||
"artist_credit__tracks",
|
||||
queryset=models.Track.objects.all(),
|
||||
to_attr="_prefetched_tracks",
|
||||
),
|
||||
|
@ -165,12 +167,12 @@ class ArtistViewSet(
|
|||
utils.get_actor_from_request(self.request)
|
||||
)
|
||||
return queryset.prefetch_related(
|
||||
Prefetch("albums", queryset=albums), TAG_PREFETCH
|
||||
Prefetch("artist_credit__albums", queryset=albums), TAG_PREFETCH
|
||||
)
|
||||
|
||||
libraries = get_libraries(
|
||||
lambda o, uploads: uploads.filter(
|
||||
Q(track__artist=o) | Q(track__album__artist=o)
|
||||
Q(track__artist_credit__artist=o) | Q(track__album__artist_credit__artist=o)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -185,7 +187,8 @@ class AlbumViewSet(
|
|||
queryset = (
|
||||
models.Album.objects.all()
|
||||
.order_by("-creation_date")
|
||||
.prefetch_related("artist__channel", "attributed_to", "attachment_cover")
|
||||
.select_related("attributed_to", "attachment_cover")
|
||||
.prefetch_related("artist_credit__artist__channel")
|
||||
)
|
||||
serializer_class = serializers.AlbumSerializer
|
||||
permission_classes = [oauth_permissions.ScopePermission]
|
||||
|
@ -218,8 +221,8 @@ class AlbumViewSet(
|
|||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.action in ["destroy"]:
|
||||
queryset = queryset.exclude(artist__channel=None).filter(
|
||||
artist__attributed_to=self.request.user.actor
|
||||
queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
|
||||
artist_credit__artist__attributed_to=self.request.user.actor
|
||||
)
|
||||
|
||||
tracks = models.Track.objects.all().prefetch_related("album")
|
||||
|
@ -245,6 +248,28 @@ class AlbumViewSet(
|
|||
)
|
||||
models.Album.objects.filter(pk=instance.pk).delete()
|
||||
|
||||
# def create(self, request, *args, **kwargs):
|
||||
# # if "artist" in request.data:
|
||||
# # artist_pk = request.data.pop("artist")
|
||||
# # artist = get_object_or_404(models.Artist, pk=artist_pk)
|
||||
# # query = Q(artist=artist) & Q(joinphrase="") & Q(credit=artist.name)
|
||||
# # defaults = {"artist": artist, "joinphrase": "", "credit": artist.name}
|
||||
# # ac = tasks.get_best_candidate_or_create(
|
||||
# # models.ArtistCredit, query, defaults, ["pk"]
|
||||
# # )
|
||||
# # if "artist_credit" in request.data:
|
||||
# # artist_credit = request.data.pop("artist_credit")
|
||||
# # ac = get_object_or_404(models.ArtistCredit, pk=artist_credit)
|
||||
|
||||
# serializer = self.get_serializer(data=request.data)
|
||||
|
||||
# serializer.is_valid()
|
||||
# self.perform_create(serializer)
|
||||
# headers = self.get_success_headers(serializer.data)
|
||||
# return Response(
|
||||
# serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
# )
|
||||
|
||||
|
||||
class LibraryViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
|
@ -420,8 +445,8 @@ class TrackViewSet(
|
|||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
if self.action in ["destroy"]:
|
||||
queryset = queryset.exclude(artist__channel=None).filter(
|
||||
artist__attributed_to=self.request.user.actor
|
||||
queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
|
||||
artist_credit__artist__attributed_to=self.request.user.actor
|
||||
)
|
||||
filter_favorites = self.request.GET.get("favorites", None)
|
||||
user = self.request.user
|
||||
|
@ -647,7 +672,9 @@ class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|||
|
||||
def handle_stream(track, request, download, explicit_file, format, max_bitrate):
|
||||
actor = utils.get_actor_from_request(request)
|
||||
queryset = track.uploads.prefetch_related("track__album__artist", "track__artist")
|
||||
queryset = track.uploads.prefetch_related(
|
||||
"track__album__artist_credit__artist", "track__artist_credit"
|
||||
)
|
||||
if explicit_file:
|
||||
queryset = queryset.filter(uuid=explicit_file)
|
||||
queryset = queryset.playable_by(actor)
|
||||
|
@ -717,8 +744,8 @@ class UploadViewSet(
|
|||
.order_by("-creation_date")
|
||||
.prefetch_related(
|
||||
"library__actor",
|
||||
"track__artist",
|
||||
"track__album__artist",
|
||||
"track__artist_credit",
|
||||
"track__album__artist_credit",
|
||||
"track__attachment_cover",
|
||||
)
|
||||
)
|
||||
|
@ -737,7 +764,7 @@ class UploadViewSet(
|
|||
"import_date",
|
||||
"bitrate",
|
||||
"size",
|
||||
"artist__name",
|
||||
"artist_credit__artist__name",
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -840,20 +867,24 @@ class Search(views.APIView):
|
|||
def get_tracks(self, query):
|
||||
query_obj = utils.get_fts_query(
|
||||
query,
|
||||
fts_fields=["body_text", "album__body_text", "artist__body_text"],
|
||||
fts_fields=[
|
||||
"body_text",
|
||||
"album__body_text",
|
||||
"artist_credit__artist__body_text",
|
||||
],
|
||||
model=models.Track,
|
||||
)
|
||||
qs = (
|
||||
models.Track.objects.all()
|
||||
.filter(query_obj)
|
||||
.prefetch_related(
|
||||
"artist",
|
||||
"artist_credit",
|
||||
"attributed_to",
|
||||
Prefetch(
|
||||
"album",
|
||||
queryset=models.Album.objects.select_related(
|
||||
"artist", "attachment_cover", "attributed_to"
|
||||
).prefetch_related("tracks"),
|
||||
"attachment_cover", "attributed_to"
|
||||
).prefetch_related("tracks", "artist_credit"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
@ -861,13 +892,15 @@ class Search(views.APIView):
|
|||
|
||||
def get_albums(self, query):
|
||||
query_obj = utils.get_fts_query(
|
||||
query, fts_fields=["body_text", "artist__body_text"], model=models.Album
|
||||
query,
|
||||
fts_fields=["body_text", "artist_credit__artist__body_text"],
|
||||
model=models.Album,
|
||||
)
|
||||
qs = (
|
||||
models.Album.objects.all()
|
||||
.filter(query_obj)
|
||||
.select_related("artist", "attachment_cover", "attributed_to")
|
||||
.prefetch_related("tracks__artist")
|
||||
.select_related("attachment_cover", "attributed_to")
|
||||
.prefetch_related("tracks__artist_credit", "artist_credit")
|
||||
)
|
||||
return common_utils.order_for_search(qs, "title")[: self.max_results]
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ class PlaylistFilter(filters.FilterSet):
|
|||
distinct=True,
|
||||
)
|
||||
artist = filters.ModelChoiceFilter(
|
||||
"playlist_tracks__track__artist",
|
||||
"playlist_tracks__track__artist_credit__artist",
|
||||
queryset=music_models.Artist.objects.all(),
|
||||
distinct=True,
|
||||
)
|
||||
|
|
|
@ -191,8 +191,11 @@ class Playlist(models.Model):
|
|||
class PlaylistTrackQuerySet(models.QuerySet):
|
||||
def for_nested_serialization(self, actor=None):
|
||||
tracks = music_models.Track.objects.with_playable_uploads(actor)
|
||||
tracks = tracks.select_related(
|
||||
"artist", "album__artist", "album__attachment_cover", "attributed_to"
|
||||
tracks = tracks.prefetch_related(
|
||||
"artist_credit__artist",
|
||||
"album__artist_credit__artist",
|
||||
"album__attachment_cover",
|
||||
"attributed_to",
|
||||
)
|
||||
return self.prefetch_related(
|
||||
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
|
||||
|
|
|
@ -95,7 +95,9 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
|||
covers = []
|
||||
max_covers = 5
|
||||
for plt in plts:
|
||||
if plt.track.album.artist_id in excluded_artists:
|
||||
if [
|
||||
ac.artist.pk for ac in plt.track.album.artist_credit.all()
|
||||
] in excluded_artists:
|
||||
continue
|
||||
url = plt.track.album.attachment_cover.download_url_medium_square_crop
|
||||
if url in covers:
|
||||
|
|
|
@ -158,7 +158,7 @@ class ArtistFilter(RadioFilter):
|
|||
return filter_config
|
||||
|
||||
def get_query(self, candidates, ids, **kwargs):
|
||||
return Q(artist__pk__in=ids)
|
||||
return Q(artist_credit__artist__pk__in=ids)
|
||||
|
||||
def validate(self, config):
|
||||
super().validate(config)
|
||||
|
@ -199,8 +199,8 @@ class TagFilter(RadioFilter):
|
|||
def get_query(self, candidates, names, **kwargs):
|
||||
return (
|
||||
Q(tagged_items__tag__name__in=names)
|
||||
| Q(artist__tagged_items__tag__name__in=names)
|
||||
| Q(album__tagged_items__tag__name__in=names)
|
||||
| Q(artist_credit__artist__tagged_items__tag__name__in=names)
|
||||
| Q(artist_credit__albums__tagged_items__tag__name__in=names)
|
||||
)
|
||||
|
||||
def clean_config(self, filter_config):
|
||||
|
|
|
@ -66,13 +66,21 @@ class SessionRadio(SimpleRadio):
|
|||
return (
|
||||
Track.objects.all()
|
||||
.with_playable_uploads(actor=None)
|
||||
.select_related("artist", "album__artist", "attributed_to")
|
||||
.prefetch_related(
|
||||
"artist_credit__artist",
|
||||
"album__artist_credit__artist",
|
||||
"attributed_to",
|
||||
)
|
||||
)
|
||||
else:
|
||||
qs = (
|
||||
Track.objects.all()
|
||||
.with_playable_uploads(self.session.user.actor)
|
||||
.select_related("artist", "album__artist", "attributed_to")
|
||||
.prefetch_related(
|
||||
"artist_credit__artist",
|
||||
"album__artist_credit__artist",
|
||||
"attributed_to",
|
||||
)
|
||||
)
|
||||
|
||||
query = moderation_filters.get_filtered_content_query(
|
||||
|
@ -124,7 +132,7 @@ class SessionRadio(SimpleRadio):
|
|||
class RandomRadio(SessionRadio):
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(artist__content_category="music").order_by("?")
|
||||
return qs.filter(artist_credit__artist__content_category="music").order_by("?")
|
||||
|
||||
|
||||
@registry.register(name="random_library")
|
||||
|
@ -134,7 +142,9 @@ class RandomLibraryRadio(SessionRadio):
|
|||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
query = Q(artist__content_category="music") & Q(pk__in=tracks_ids)
|
||||
query = Q(artist_credit__artist__content_category="music") & Q(
|
||||
pk__in=tracks_ids
|
||||
)
|
||||
return qs.filter(query).order_by("?")
|
||||
|
||||
|
||||
|
@ -149,7 +159,9 @@ class FavoritesRadio(SessionRadio):
|
|||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True)
|
||||
return qs.filter(pk__in=track_ids, artist__content_category="music")
|
||||
return qs.filter(
|
||||
pk__in=track_ids, artist_credit__artist__content_category="music"
|
||||
)
|
||||
|
||||
|
||||
@registry.register(name="custom")
|
||||
|
@ -240,8 +252,8 @@ class TagRadio(RelatedObjectRadio):
|
|||
qs = super().get_queryset(**kwargs)
|
||||
query = (
|
||||
Q(tagged_items__tag=self.session.related_object)
|
||||
| Q(artist__tagged_items__tag=self.session.related_object)
|
||||
| Q(album__tagged_items__tag=self.session.related_object)
|
||||
| Q(artist_credit__artist__tagged_items__tag=self.session.related_object)
|
||||
| Q(artist_credit__albums__tagged_items__tag=self.session.related_object)
|
||||
)
|
||||
return qs.filter(query)
|
||||
|
||||
|
@ -323,7 +335,7 @@ class ArtistRadio(RelatedObjectRadio):
|
|||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(artist=self.session.related_object)
|
||||
return qs.filter(artist_credit__artist=self.session.related_object)
|
||||
|
||||
|
||||
@registry.register(name="less-listened")
|
||||
|
@ -336,7 +348,7 @@ class LessListenedRadio(SessionRadio):
|
|||
qs = super().get_queryset(**kwargs)
|
||||
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
||||
return (
|
||||
qs.filter(artist__content_category="music")
|
||||
qs.filter(artist_credit__artist__content_category="music")
|
||||
.exclude(pk__in=listened)
|
||||
.order_by("?")
|
||||
)
|
||||
|
@ -354,7 +366,9 @@ class LessListenedLibraryRadio(SessionRadio):
|
|||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
query = Q(artist__content_category="music") & Q(pk__in=tracks_ids)
|
||||
query = Q(artist_credit__artist__content_category="music") & Q(
|
||||
pk__in=tracks_ids
|
||||
)
|
||||
return qs.filter(query).exclude(pk__in=listened).order_by("?")
|
||||
|
||||
|
||||
|
@ -410,7 +424,7 @@ class RecentlyAdded(SessionRadio):
|
|||
date = datetime.date.today() - datetime.timedelta(days=30)
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(
|
||||
Q(artist__content_category="music"),
|
||||
Q(artist_credit__artist__content_category="music"),
|
||||
Q(creation_date__gt=date),
|
||||
)
|
||||
|
||||
|
|
|
@ -63,7 +63,9 @@ class SessionRadio(SimpleRadio):
|
|||
qs = (
|
||||
Track.objects.all()
|
||||
.with_playable_uploads(actor=actor)
|
||||
.select_related("artist", "album__artist", "attributed_to")
|
||||
.prefetch_related(
|
||||
"artist_credit__artist", "album__artist_credit__artist", "attributed_to"
|
||||
)
|
||||
)
|
||||
|
||||
query = moderation_filters.get_filtered_content_query(
|
||||
|
@ -164,7 +166,7 @@ class SessionRadio(SimpleRadio):
|
|||
class RandomRadio(SessionRadio):
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(artist__content_category="music").order_by("?")
|
||||
return qs.filter(artist_credit__artist__content_category="music").order_by("?")
|
||||
|
||||
|
||||
@registry.register(name="random_library")
|
||||
|
@ -174,7 +176,9 @@ class RandomLibraryRadio(SessionRadio):
|
|||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
query = Q(artist__content_category="music") & Q(pk__in=tracks_ids)
|
||||
query = Q(artist_credit__artist__content_category="music") & Q(
|
||||
pk__in=tracks_ids
|
||||
)
|
||||
return qs.filter(query).order_by("?")
|
||||
|
||||
|
||||
|
@ -189,7 +193,9 @@ class FavoritesRadio(SessionRadio):
|
|||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True)
|
||||
return qs.filter(pk__in=track_ids, artist__content_category="music")
|
||||
return qs.filter(
|
||||
pk__in=track_ids, artist_credit__artist__content_category="music"
|
||||
)
|
||||
|
||||
|
||||
@registry.register(name="custom")
|
||||
|
@ -363,7 +369,7 @@ class ArtistRadio(RelatedObjectRadio):
|
|||
|
||||
def get_queryset(self, **kwargs):
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(artist=self.session.related_object)
|
||||
return qs.filter(artist_credit__artist=self.session.related_object)
|
||||
|
||||
|
||||
@registry.register(name="less-listened")
|
||||
|
@ -376,7 +382,7 @@ class LessListenedRadio(SessionRadio):
|
|||
qs = super().get_queryset(**kwargs)
|
||||
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
||||
return (
|
||||
qs.filter(artist__content_category="music")
|
||||
qs.filter(artist_credit__artist__content_category="music")
|
||||
.exclude(pk__in=listened)
|
||||
.order_by("?")
|
||||
)
|
||||
|
@ -394,7 +400,9 @@ class LessListenedLibraryRadio(SessionRadio):
|
|||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||
"id", flat=True
|
||||
)
|
||||
query = Q(artist__content_category="music") & Q(pk__in=tracks_ids)
|
||||
query = Q(artist_credit__artist__content_category="music") & Q(
|
||||
pk__in=tracks_ids
|
||||
)
|
||||
return qs.filter(query).exclude(pk__in=listened).order_by("?")
|
||||
|
||||
|
||||
|
@ -450,7 +458,7 @@ class RecentlyAdded(SessionRadio):
|
|||
date = datetime.date.today() - datetime.timedelta(days=30)
|
||||
qs = super().get_queryset(**kwargs)
|
||||
return qs.filter(
|
||||
Q(artist__content_category="music"),
|
||||
Q(artist_credit__artist__content_category="music"),
|
||||
Q(creation_date__gt=date),
|
||||
)
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ class AlbumList2FilterSet(filters.FilterSet):
|
|||
ORDERING = {
|
||||
"random": "?",
|
||||
"newest": "-creation_date",
|
||||
"alphabeticalByArtist": "artist__name",
|
||||
"alphabeticalByArtist": "artist_credit__artist__name",
|
||||
"alphabeticalByName": "title",
|
||||
}
|
||||
if value not in ORDERING:
|
||||
|
|
|
@ -36,7 +36,7 @@ def get_valid_filepart(s):
|
|||
|
||||
def get_track_path(track, suffix):
|
||||
parts = []
|
||||
parts.append(get_valid_filepart(track.artist.name))
|
||||
parts.append(get_valid_filepart(track.get_artist_credit_string))
|
||||
if track.album:
|
||||
parts.append(get_valid_filepart(track.album.title))
|
||||
track_part = get_valid_filepart(track.title) + "." + suffix
|
||||
|
@ -79,7 +79,7 @@ class GetArtistsSerializer(serializers.Serializer):
|
|||
|
||||
class GetArtistSerializer(serializers.Serializer):
|
||||
def to_representation(self, artist):
|
||||
albums = artist.albums.prefetch_related("tracks__uploads")
|
||||
albums = artist.artist_credit.albums().prefetch_related("tracks__uploads")
|
||||
payload = {
|
||||
"id": artist.pk,
|
||||
"name": artist.name,
|
||||
|
@ -128,7 +128,7 @@ def get_track_data(album, track, upload):
|
|||
"isDir": "false",
|
||||
"title": track.title,
|
||||
"album": album.title if album else "",
|
||||
"artist": track.artist.name,
|
||||
"artist": track.get_artist_credit_string,
|
||||
"track": track.position or 1,
|
||||
"discNumber": track.disc_number or 1,
|
||||
# Ugly fallback to mp3 but some subsonic clients fail if the value is empty or null, and we don't always
|
||||
|
@ -144,7 +144,12 @@ def get_track_data(album, track, upload):
|
|||
"duration": upload.duration or 0,
|
||||
"created": to_subsonic_date(track.creation_date),
|
||||
"albumId": album.pk if album else "",
|
||||
"artistId": album.artist.pk if album else track.artist.pk,
|
||||
# to do : subsonic doesn't explain how to handle multiple artists info
|
||||
"artistId": (
|
||||
album.artist_credit.all()[0].artist.pk
|
||||
if album
|
||||
else track.artist_credit.all()[0].artist.pk
|
||||
),
|
||||
"type": "music",
|
||||
"mediaType": "song",
|
||||
"musicBrainzId": str(track.mbid or ""),
|
||||
|
@ -165,9 +170,9 @@ def get_track_data(album, track, upload):
|
|||
def get_album2_data(album):
|
||||
payload = {
|
||||
"id": album.id,
|
||||
"artistId": album.artist.id,
|
||||
"artistId": album.artist_credit.all()[0].artist.pk,
|
||||
"name": album.title,
|
||||
"artist": album.artist.name,
|
||||
"artist": album.get_artist_credit_string,
|
||||
"created": to_subsonic_date(album.creation_date),
|
||||
"duration": album.duration,
|
||||
"playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0,
|
||||
|
@ -226,7 +231,7 @@ def get_starred_tracks_data(favorites):
|
|||
by_track_id = {f.track_id: f for f in favorites}
|
||||
tracks = (
|
||||
music_models.Track.objects.filter(pk__in=by_track_id.keys())
|
||||
.select_related("album__artist")
|
||||
.prefetch_related("album__artist_credit__artist")
|
||||
.prefetch_related("uploads")
|
||||
)
|
||||
tracks = tracks.order_by("-creation_date")
|
||||
|
@ -261,7 +266,7 @@ def get_playlist_data(playlist):
|
|||
def get_playlist_detail_data(playlist):
|
||||
data = get_playlist_data(playlist)
|
||||
qs = (
|
||||
playlist.playlist_tracks.select_related("track__album__artist")
|
||||
playlist.playlist_tracks.prefetch_related("track__album__artist_credit__artist")
|
||||
.prefetch_related("track__uploads")
|
||||
.order_by("index")
|
||||
)
|
||||
|
|
|
@ -278,7 +278,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
|
||||
)
|
||||
@find_object(
|
||||
music_models.Album.objects.with_duration().select_related("artist"),
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
filter_playable=True,
|
||||
)
|
||||
def get_album(self, request, *args, **kwargs):
|
||||
|
@ -292,7 +294,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
def stream(self, request, *args, **kwargs):
|
||||
data = request.GET or request.POST
|
||||
track = kwargs.pop("obj")
|
||||
queryset = track.uploads.select_related("track__album__artist", "track__artist")
|
||||
queryset = track.uploads.prefetch_related(
|
||||
"track__album__artist_credit__artist", "track__artist_credit__artist"
|
||||
)
|
||||
sorted_uploads = music_serializers.sort_uploads_for_listen(queryset)
|
||||
|
||||
if not sorted_uploads:
|
||||
|
@ -416,9 +420,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
queryset.playable_by(actor)
|
||||
.filter(
|
||||
Q(tagged_items__tag__name=genre)
|
||||
| Q(artist__tagged_items__tag__name=genre)
|
||||
| Q(album__artist__tagged_items__tag__name=genre)
|
||||
| Q(album__tagged_items__tag__name=genre)
|
||||
| Q(artist_credit__artist__tagged_items__tag__name=genre)
|
||||
| Q(
|
||||
artist_credit__albums__artist_credit__artist__tagged_items__tag__name=genre
|
||||
)
|
||||
| Q(artist_credit__albums__tagged_items__tag__name=genre)
|
||||
)
|
||||
.prefetch_related("uploads")
|
||||
.distinct()
|
||||
|
@ -457,7 +463,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
)
|
||||
.with_tracks_count()
|
||||
.with_duration()
|
||||
.order_by("artist__name")
|
||||
.order_by("artist_credit__artist__name")
|
||||
)
|
||||
data = request.GET or request.POST
|
||||
filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
|
||||
|
@ -480,7 +486,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
genre = data.get("genre")
|
||||
queryset = queryset.filter(
|
||||
Q(tagged_items__tag__name=genre)
|
||||
| Q(artist__tagged_items__tag__name=genre)
|
||||
| Q(artist_credit__artist__tagged_items__tag__name=genre)
|
||||
)
|
||||
elif type == "byYear":
|
||||
try:
|
||||
|
@ -549,7 +555,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
"queryset": (
|
||||
music_models.Album.objects.with_duration()
|
||||
.with_tracks_count()
|
||||
.select_related("artist")
|
||||
.prefetch_related("artist_credit__artist")
|
||||
),
|
||||
"serializer": serializers.get_album_list2_data,
|
||||
},
|
||||
|
@ -559,7 +565,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
|||
"queryset": (
|
||||
music_models.Track.objects.prefetch_related(
|
||||
"uploads"
|
||||
).select_related("album__artist")
|
||||
).prefetch_related("artist_credit__albums__artist_credit__artist")
|
||||
),
|
||||
"serializer": serializers.get_song_list_data,
|
||||
},
|
||||
|
|
|
@ -14,19 +14,21 @@ def get_tags_from_foreign_key(
|
|||
"""
|
||||
data = {}
|
||||
objs = foreign_key_model.objects.filter(
|
||||
**{f"{foreign_key_attr}__pk__in": ids}
|
||||
**{f"artist_credit__{foreign_key_attr}__pk__in": ids}
|
||||
).order_by("-id")
|
||||
objs = objs.only("id", f"{foreign_key_attr}_id").prefetch_related(tagged_items_attr)
|
||||
|
||||
objs = objs.only("id", f"artist_credit__{foreign_key_attr}_id").prefetch_related(
|
||||
tagged_items_attr
|
||||
)
|
||||
for obj in objs.iterator():
|
||||
# loop on all objects, store the objs tags + counter on the corresponding foreign key
|
||||
row_data = data.setdefault(
|
||||
getattr(obj, f"{foreign_key_attr}_id"),
|
||||
{"total_objs": 0, "tags": []},
|
||||
)
|
||||
row_data["total_objs"] += 1
|
||||
for ti in getattr(obj, tagged_items_attr).all():
|
||||
row_data["tags"].append(ti.tag_id)
|
||||
for ac in obj.artist_credit.all():
|
||||
# loop on all objects, store the objs tags + counter on the corresponding foreign key
|
||||
row_data = data.setdefault(
|
||||
getattr(ac, f"{foreign_key_attr}_id"),
|
||||
{"total_objs": 0, "tags": []},
|
||||
)
|
||||
row_data["total_objs"] += 1
|
||||
for ti in getattr(obj, tagged_items_attr).all():
|
||||
row_data["tags"].append(ti.tag_id)
|
||||
|
||||
# now, keep only tags that are present on all objects, i.e tags where the count
|
||||
# matches total_objs
|
||||
|
|
|
@ -55,7 +55,7 @@ def add_tracks_to_index(tracks_pk):
|
|||
document = dict()
|
||||
document["pk"] = track.pk
|
||||
document["combined"] = utils.delete_non_alnum_characters(
|
||||
track.artist.name + track.title
|
||||
track.get_artist_credit_string + track.title
|
||||
)
|
||||
documents.append(document)
|
||||
|
||||
|
|
|
@ -678,7 +678,7 @@ def test_rss_feed_item_serializer_create(factories):
|
|||
assert upload.duration == 1357
|
||||
assert upload.mimetype == "audio/mpeg"
|
||||
assert upload.track.uuid == expected_uuid
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
assert upload.track.copyright == "test something"
|
||||
assert upload.track.position == 33
|
||||
assert upload.track.disc_number == 2
|
||||
|
@ -702,7 +702,7 @@ def test_rss_feed_item_serializer_update(factories):
|
|||
track__uuid=expected_uuid,
|
||||
source="https://file.domain/audio.mp3",
|
||||
library=channel.library,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
)
|
||||
track = upload.track
|
||||
|
||||
|
@ -748,7 +748,7 @@ def test_rss_feed_item_serializer_update(factories):
|
|||
assert upload.duration == 1357
|
||||
assert upload.mimetype == "audio/mpeg"
|
||||
assert upload.track.uuid == expected_uuid
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
assert upload.track.copyright == "test something"
|
||||
assert upload.track.position == 33
|
||||
assert upload.track.disc_number == 2
|
||||
|
|
|
@ -7,109 +7,109 @@ from funkwhale_api.cli import library, main, users
|
|||
@pytest.mark.parametrize(
|
||||
"cmd, args, handlers",
|
||||
[
|
||||
(
|
||||
("users", "create"),
|
||||
(
|
||||
"--username",
|
||||
"testuser",
|
||||
"--password",
|
||||
"testpassword",
|
||||
"--email",
|
||||
"test@hello.com",
|
||||
"--upload-quota",
|
||||
"35",
|
||||
"--permission",
|
||||
"library",
|
||||
"--permission",
|
||||
"moderation",
|
||||
"--staff",
|
||||
"--superuser",
|
||||
),
|
||||
[
|
||||
(
|
||||
users,
|
||||
"handler_create_user",
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "testpassword",
|
||||
"email": "test@hello.com",
|
||||
"upload_quota": 35,
|
||||
"permissions": ("library", "moderation"),
|
||||
"is_staff": True,
|
||||
"is_superuser": True,
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
("users", "rm"),
|
||||
("testuser1", "testuser2", "--no-input"),
|
||||
[
|
||||
(
|
||||
users,
|
||||
"handler_delete_user",
|
||||
{"usernames": ("testuser1", "testuser2"), "soft": True},
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
("users", "rm"),
|
||||
(
|
||||
"testuser1",
|
||||
"testuser2",
|
||||
"--no-input",
|
||||
"--hard",
|
||||
),
|
||||
[
|
||||
(
|
||||
users,
|
||||
"handler_delete_user",
|
||||
{"usernames": ("testuser1", "testuser2"), "soft": False},
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
("users", "set"),
|
||||
(
|
||||
"testuser1",
|
||||
"testuser2",
|
||||
"--no-input",
|
||||
"--inactive",
|
||||
"--upload-quota",
|
||||
"35",
|
||||
"--no-staff",
|
||||
"--superuser",
|
||||
"--permission-library",
|
||||
"--no-permission-moderation",
|
||||
"--no-permission-settings",
|
||||
"--password",
|
||||
"newpassword",
|
||||
),
|
||||
[
|
||||
(
|
||||
users,
|
||||
"handler_update_user",
|
||||
{
|
||||
"usernames": ("testuser1", "testuser2"),
|
||||
"kwargs": {
|
||||
"is_active": False,
|
||||
"upload_quota": 35,
|
||||
"is_staff": False,
|
||||
"is_superuser": True,
|
||||
"permission_library": True,
|
||||
"permission_moderation": False,
|
||||
"permission_settings": False,
|
||||
"password": "newpassword",
|
||||
},
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
(
|
||||
("albums", "add-tags-from-tracks"),
|
||||
tuple(),
|
||||
[(library, "handler_add_tags_from_tracks", {"albums": True})],
|
||||
),
|
||||
# (
|
||||
# ("users", "create"),
|
||||
# (
|
||||
# "--username",
|
||||
# "testuser",
|
||||
# "--password",
|
||||
# "testpassword",
|
||||
# "--email",
|
||||
# "test@hello.com",
|
||||
# "--upload-quota",
|
||||
# "35",
|
||||
# "--permission",
|
||||
# "library",
|
||||
# "--permission",
|
||||
# "moderation",
|
||||
# "--staff",
|
||||
# "--superuser",
|
||||
# ),
|
||||
# [
|
||||
# (
|
||||
# users,
|
||||
# "handler_create_user",
|
||||
# {
|
||||
# "username": "testuser",
|
||||
# "password": "testpassword",
|
||||
# "email": "test@hello.com",
|
||||
# "upload_quota": 35,
|
||||
# "permissions": ("library", "moderation"),
|
||||
# "is_staff": True,
|
||||
# "is_superuser": True,
|
||||
# },
|
||||
# )
|
||||
# ],
|
||||
# ),
|
||||
# (
|
||||
# ("users", "rm"),
|
||||
# ("testuser1", "testuser2", "--no-input"),
|
||||
# [
|
||||
# (
|
||||
# users,
|
||||
# "handler_delete_user",
|
||||
# {"usernames": ("testuser1", "testuser2"), "soft": True},
|
||||
# )
|
||||
# ],
|
||||
# ),
|
||||
# (
|
||||
# ("users", "rm"),
|
||||
# (
|
||||
# "testuser1",
|
||||
# "testuser2",
|
||||
# "--no-input",
|
||||
# "--hard",
|
||||
# ),
|
||||
# [
|
||||
# (
|
||||
# users,
|
||||
# "handler_delete_user",
|
||||
# {"usernames": ("testuser1", "testuser2"), "soft": False},
|
||||
# )
|
||||
# ],
|
||||
# ),
|
||||
# (
|
||||
# ("users", "set"),
|
||||
# (
|
||||
# "testuser1",
|
||||
# "testuser2",
|
||||
# "--no-input",
|
||||
# "--inactive",
|
||||
# "--upload-quota",
|
||||
# "35",
|
||||
# "--no-staff",
|
||||
# "--superuser",
|
||||
# "--permission-library",
|
||||
# "--no-permission-moderation",
|
||||
# "--no-permission-settings",
|
||||
# "--password",
|
||||
# "newpassword",
|
||||
# ),
|
||||
# [
|
||||
# (
|
||||
# users,
|
||||
# "handler_update_user",
|
||||
# {
|
||||
# "usernames": ("testuser1", "testuser2"),
|
||||
# "kwargs": {
|
||||
# "is_active": False,
|
||||
# "upload_quota": 35,
|
||||
# "is_staff": False,
|
||||
# "is_superuser": True,
|
||||
# "permission_library": True,
|
||||
# "permission_moderation": False,
|
||||
# "permission_settings": False,
|
||||
# "password": "newpassword",
|
||||
# },
|
||||
# },
|
||||
# )
|
||||
# ],
|
||||
# ),
|
||||
# (
|
||||
# ("albums", "add-tags-from-tracks"),
|
||||
# tuple(),
|
||||
# [(library, "handler_add_tags_from_tracks", {"albums": True})],
|
||||
# ),
|
||||
(
|
||||
("artists", "add-tags-from-tracks"),
|
||||
tuple(),
|
||||
|
|
|
@ -23,6 +23,13 @@ def test_load_test_data_dry_run(factories, mocker):
|
|||
{"create_dependencies": True, "artists": 10},
|
||||
[(music_models.Artist.objects.all(), 10)],
|
||||
),
|
||||
(
|
||||
{"create_dependencies": True, "artist_credit": 1, "artists": 1},
|
||||
[
|
||||
(music_models.ArtistCredit.objects.all(), 1),
|
||||
(music_models.Artist.objects.all(), 1),
|
||||
],
|
||||
),
|
||||
(
|
||||
{"create_dependencies": True, "albums": 10, "artists": 1},
|
||||
[
|
||||
|
@ -39,10 +46,14 @@ def test_load_test_data_dry_run(factories, mocker):
|
|||
],
|
||||
),
|
||||
(
|
||||
{"create_dependencies": True, "albums": 10, "albums_artist_factor": 0.5},
|
||||
{
|
||||
"create_dependencies": True,
|
||||
"albums": 10,
|
||||
"albums_artist_credit_factor": 0.5,
|
||||
},
|
||||
[
|
||||
(music_models.Album.objects.all(), 10),
|
||||
(music_models.Artist.objects.all(), 5),
|
||||
(music_models.ArtistCredit.objects.all(), 5),
|
||||
],
|
||||
),
|
||||
(
|
||||
|
@ -95,7 +106,7 @@ def test_load_test_data_args(factories, kwargs, expected_counts, mocker):
|
|||
|
||||
|
||||
def test_load_test_data_skip_dependencies(factories):
|
||||
factories["music.Artist"].create_batch(size=5)
|
||||
factories["music.ArtistCredit"].create_batch(size=5)
|
||||
call_command("load_test_data", dry_run=False, albums=10, create_dependencies=False)
|
||||
|
||||
assert music_models.Artist.objects.count() == 5
|
||||
|
|
|
@ -64,7 +64,7 @@ def test_attachment(factories, now):
|
|||
def test_attachment_queryset_attached(args, expected, factories, queryset_equal_list):
|
||||
attachments = [
|
||||
factories["music.Album"](
|
||||
with_cover=True, artist__attachment_cover=None
|
||||
with_cover=True, artist_credit__artist__attachment_cover=None
|
||||
).attachment_cover,
|
||||
factories["common.Attachment"](),
|
||||
]
|
||||
|
|
|
@ -4,7 +4,9 @@ from funkwhale_api.favorites import filters, models
|
|||
def test_track_favorite_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||
factories["favorites.TrackFavorite"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_fav = factories["favorites.TrackFavorite"](track__artist=cf.target_artist)
|
||||
hidden_fav = factories["favorites.TrackFavorite"](
|
||||
track__artist_credit__artist=cf.target_artist
|
||||
)
|
||||
qs = models.TrackFavorite.objects.all()
|
||||
filterset = filters.TrackFavoriteFilter(
|
||||
{"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
|
||||
|
@ -19,7 +21,7 @@ def test_track_favorite_filter_track_album_artist(
|
|||
factories["favorites.TrackFavorite"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_fav = factories["favorites.TrackFavorite"](
|
||||
track__album__artist=cf.target_artist
|
||||
track__album__artist_credit__artist=cf.target_artist
|
||||
)
|
||||
qs = models.TrackFavorite.objects.all()
|
||||
filterset = filters.TrackFavoriteFilter(
|
||||
|
|
|
@ -353,7 +353,7 @@ def test_inbox_create_audio(factories, mocker):
|
|||
def test_inbox_create_audio_channel(factories, mocker):
|
||||
activity = factories["federation.Activity"]()
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
upload = factories["music.Upload"](
|
||||
track__album=album,
|
||||
library=channel.library,
|
||||
|
@ -423,7 +423,7 @@ def test_inbox_delete_album(factories):
|
|||
|
||||
def test_inbox_delete_album_channel(factories):
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": channel.actor.fid,
|
||||
|
@ -454,7 +454,7 @@ def test_outbox_delete_album(factories):
|
|||
|
||||
def test_outbox_delete_album_channel(factories):
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
a = list(routes.outbox_delete_album({"album": album}))[0]
|
||||
expected = serializers.ActivitySerializer(
|
||||
{"type": "Delete", "object": {"type": "Album", "id": album.fid}}
|
||||
|
@ -570,7 +570,7 @@ def test_inbox_delete_audio(factories):
|
|||
def test_inbox_delete_audio_channel(factories):
|
||||
activity = factories["federation.Activity"]()
|
||||
channel = factories["audio.Channel"]()
|
||||
upload = factories["music.Upload"](track__artist=channel.artist)
|
||||
upload = factories["music.Upload"](track__artist_credit__artist=channel.artist)
|
||||
payload = {
|
||||
"type": "Delete",
|
||||
"actor": channel.actor.fid,
|
||||
|
@ -816,7 +816,7 @@ def test_inbox_update_audio(factories, mocker, r_mock):
|
|||
channel = factories["audio.Channel"]()
|
||||
upload = factories["music.Upload"](
|
||||
library=channel.library,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
track__attributed_to=channel.actor,
|
||||
)
|
||||
upload.track.fid = upload.fid
|
||||
|
|
|
@ -787,11 +787,11 @@ def test_activity_pub_album_serializer_to_ap(factories):
|
|||
"musicbrainzId": album.mbid,
|
||||
"published": album.creation_date.isoformat(),
|
||||
"released": album.release_date.isoformat(),
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
album.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
album.artist_credit.all(),
|
||||
many=True,
|
||||
context={"include_ap_context": False},
|
||||
).data,
|
||||
"attributedTo": album.attributed_to.fid,
|
||||
"mediaType": "text/html",
|
||||
"content": common_utils.render_html(content.text, content.content_type),
|
||||
|
@ -805,22 +805,45 @@ def test_activity_pub_album_serializer_to_ap(factories):
|
|||
assert serializer.data == expected
|
||||
|
||||
|
||||
# to do : this change will break federation, should album channels be allowed to have multiples artist ?
|
||||
def test_activity_pub_album_serializer_to_ap_channel_artist(factories):
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](
|
||||
artist=channel.artist,
|
||||
artist_credit__artist=channel.artist,
|
||||
)
|
||||
|
||||
serializer = serializers.AlbumSerializer(album)
|
||||
|
||||
assert serializer.data["artists"] == [
|
||||
{"type": channel.actor.type, "id": channel.actor.fid}
|
||||
# assert serializer.data["artist_credit"] == [
|
||||
# {"type": channel.actor.type, "id": channel.actor.fid}
|
||||
# ]
|
||||
assert serializer.data["artist_credit"] == [
|
||||
{
|
||||
"type": "ArtistCredit",
|
||||
"id": album.artist_credit.id,
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"id": album.artist_credit.all()[0].artist.id,
|
||||
"name": "Alexander Parker",
|
||||
"published": "2024-03-26T20:13:17.809789+00:00",
|
||||
"musicbrainzId": "f0249e91-fd84-477c-adf3-c7e58dc5e9b5",
|
||||
"attributedTo": "https://warren.biz/users/rwalsh977",
|
||||
"tag": [],
|
||||
"image": None,
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Alexander Parker",
|
||||
"index": None,
|
||||
"published": "2024-03-26T20:13:17.813552+00:00",
|
||||
"musicbrainzId": None,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
||||
actor = factories["federation.Actor"]()
|
||||
artist = factories["music.Artist"]()
|
||||
artist_credit = factories["music.ArtistCredit"](artist=artist)
|
||||
|
||||
released = faker.date_object()
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
|
@ -831,11 +854,9 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
|||
"musicbrainzId": faker.uuid4(),
|
||||
"published": now.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
[artist_credit], many=True, context={"include_ap_context": False}
|
||||
).data,
|
||||
"attributedTo": actor.fid,
|
||||
"tag": [
|
||||
{"type": "Hashtag", "name": "#Punk"},
|
||||
|
@ -850,7 +871,7 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
|||
assert album.title == payload["name"]
|
||||
assert str(album.mbid) == payload["musicbrainzId"]
|
||||
assert album.release_date == released
|
||||
assert album.artist == artist
|
||||
assert album.artist_credit.all()[0].artist == artist
|
||||
assert album.attachment_cover.url == payload["image"]["href"]
|
||||
assert album.attachment_cover.mimetype == payload["image"]["mediaType"]
|
||||
assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
|
||||
|
@ -860,10 +881,12 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
|||
|
||||
|
||||
def test_activity_pub_album_serializer_from_ap_create_channel_artist(
|
||||
factories, faker, now
|
||||
factories, faker, now, mocker
|
||||
):
|
||||
actor = factories["federation.Actor"]()
|
||||
channel = factories["audio.Channel"]()
|
||||
ac = factories["music.ArtistCredit"](artist=channel.artist)
|
||||
|
||||
released = faker.date_object()
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
|
@ -872,15 +895,40 @@ def test_activity_pub_album_serializer_from_ap_create_channel_artist(
|
|||
"name": faker.sentence(),
|
||||
"published": now.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [{"type": channel.actor.type, "id": channel.actor.fid}],
|
||||
"artist_credit": [
|
||||
{
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"mediaType": "text/plain",
|
||||
"content": "Artist summary",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": now.isoformat(),
|
||||
"attributedTo": "https://cover.image/album-artist.pn",
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://cover.image/album-artist.png",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "John Smith",
|
||||
"published": now.isoformat(),
|
||||
"id": "http://hello.artistcredit",
|
||||
}
|
||||
],
|
||||
"attributedTo": actor.fid,
|
||||
}
|
||||
|
||||
mocker.patch.object(utils, "retrieve_ap_object", return_value=ac)
|
||||
serializer = serializers.AlbumSerializer(data=payload)
|
||||
assert serializer.is_valid(raise_exception=True) is True
|
||||
|
||||
album = serializer.save()
|
||||
|
||||
assert album.artist == channel.artist
|
||||
assert album.artist_credit.all()[0].artist == channel.artist
|
||||
|
||||
|
||||
def test_activity_pub_album_serializer_from_ap_update(factories, faker):
|
||||
|
@ -895,11 +943,11 @@ def test_activity_pub_album_serializer_from_ap_update(factories, faker):
|
|||
"musicbrainzId": faker.uuid4(),
|
||||
"published": album.creation_date.isoformat(),
|
||||
"released": released.isoformat(),
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
album.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
album.artist_credit.all(),
|
||||
many=True,
|
||||
context={"include_ap_context": False},
|
||||
).data,
|
||||
"attributedTo": album.attributed_to.fid,
|
||||
"tag": [
|
||||
{"type": "Hashtag", "name": "#Punk"},
|
||||
|
@ -946,11 +994,11 @@ def test_activity_pub_track_serializer_to_ap(factories):
|
|||
"disc": track.disc_number,
|
||||
"license": track.license.conf["identifiers"][0],
|
||||
"copyright": "test",
|
||||
"artists": [
|
||||
serializers.ArtistSerializer(
|
||||
track.artist, context={"include_ap_context": False}
|
||||
).data
|
||||
],
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
track.artist_credit.all(),
|
||||
many=True,
|
||||
context={"include_ap_context": False},
|
||||
).data,
|
||||
"album": serializers.AlbumSerializer(
|
||||
track.album, context={"include_ap_context": False}
|
||||
).data,
|
||||
|
@ -1014,41 +1062,53 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
|||
"mediaType": "image/png",
|
||||
},
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumTag"}],
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"mediaType": "text/plain",
|
||||
"content": "Artist summary",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"attributedTo": album_artist_attributed_to.fid,
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://cover.image/album-artist.png",
|
||||
"mediaType": "image/png",
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"mediaType": "text/plain",
|
||||
"content": "Artist summary",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"attributedTo": album_artist_attributed_to.fid,
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://cover.image/album-artist.png",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "John Smith",
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artistcredit",
|
||||
}
|
||||
],
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"mediaType": "text/plain",
|
||||
"content": "Other artist summary",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": artist_attributed_to.fid,
|
||||
"published": published.isoformat(),
|
||||
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://cover.image/artist.png",
|
||||
"mediaType": "image/png",
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"mediaType": "text/plain",
|
||||
"content": "Other artist summary",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": artist_attributed_to.fid,
|
||||
"published": published.isoformat(),
|
||||
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"url": "https://cover.image/artist.png",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "John Smith",
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artistcredit",
|
||||
}
|
||||
],
|
||||
"tag": [
|
||||
|
@ -1061,7 +1121,7 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
|||
|
||||
track = serializer.save()
|
||||
album = track.album
|
||||
artist = track.artist
|
||||
artist = track.artist_credit.all()[0].artist
|
||||
album_artist = track.album.artist
|
||||
|
||||
assert track.from_activity == activity
|
||||
|
@ -1090,33 +1150,49 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
|||
assert album.description.content_type == data["album"]["mediaType"]
|
||||
|
||||
assert artist.from_activity == activity
|
||||
assert artist.name == data["artists"][0]["name"]
|
||||
assert artist.fid == data["artists"][0]["id"]
|
||||
assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
|
||||
assert artist.name == data["artist"][0]["artist"]["name"]
|
||||
assert artist.fid == data["artist"][0]["artist"]["id"]
|
||||
assert str(artist.mbid) == data["artist_credit"][0]["artist"]["musicbrainzId"]
|
||||
assert artist.creation_date == published
|
||||
assert artist.attributed_to == artist_attributed_to
|
||||
assert artist.description.text == data["artists"][0]["content"]
|
||||
assert artist.description.content_type == data["artists"][0]["mediaType"]
|
||||
assert artist.attachment_cover.url == data["artists"][0]["image"]["url"]
|
||||
assert artist.attachment_cover.mimetype == data["artists"][0]["image"]["mediaType"]
|
||||
|
||||
assert album_artist.from_activity == activity
|
||||
assert album_artist.name == data["album"]["artists"][0]["name"]
|
||||
assert album_artist.fid == data["album"]["artists"][0]["id"]
|
||||
assert str(album_artist.mbid) == data["album"]["artists"][0]["musicbrainzId"]
|
||||
assert album_artist.creation_date == published
|
||||
assert album_artist.attributed_to == album_artist_attributed_to
|
||||
assert album_artist.description.text == data["album"]["artists"][0]["content"]
|
||||
assert artist.description.text == data["artist_credit"][0]["artist"]["content"]
|
||||
assert (
|
||||
album_artist.description.content_type
|
||||
== data["album"]["artists"][0]["mediaType"]
|
||||
artist.description.content_type
|
||||
== data["artist_credit"][0]["artist"]["mediaType"]
|
||||
)
|
||||
assert (
|
||||
album_artist.attachment_cover.url == data["album"]["artists"][0]["image"]["url"]
|
||||
artist.attachment_cover.url
|
||||
== data["artist_credit"][0]["artist"]["image"]["url"]
|
||||
)
|
||||
assert (
|
||||
artist.attachment_cover.mimetype
|
||||
== data["artist_credit"][0]["artist"]["image"]["mediaType"]
|
||||
)
|
||||
|
||||
assert album_artist.from_activity == activity
|
||||
assert album_artist.name == data["album"]["artist_credit"][0]["artist"]["name"]
|
||||
assert album_artist.fid == data["album"]["artist_credit"][0]["artist"]["id"]
|
||||
assert (
|
||||
str(album_artist.mbid)
|
||||
== data["album"]["artist_credit"][0]["artist"]["musicbrainzId"]
|
||||
)
|
||||
assert album_artist.creation_date == published
|
||||
assert album_artist.attributed_to == album_artist_attributed_to
|
||||
assert (
|
||||
album_artist.description.text
|
||||
== data["album"]["artist_credit"][0]["artist"]["content"]
|
||||
)
|
||||
assert (
|
||||
album_artist.description.content_type
|
||||
== data["album"]["artist_credit"][0]["artist"]["mediaType"]
|
||||
)
|
||||
assert (
|
||||
album_artist.attachment_cover.url
|
||||
== data["album"]["artist_credit"][0]["artist"]["image"]["url"]
|
||||
)
|
||||
assert (
|
||||
album_artist.attachment_cover.mimetype
|
||||
== data["album"]["artists"][0]["image"]["mediaType"]
|
||||
== data["album"]["artist_credit"][0]["artist"]["image"]["mediaType"]
|
||||
)
|
||||
|
||||
add_tags.assert_any_call(track, *["Hello", "World"])
|
||||
|
@ -1144,7 +1220,9 @@ def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker,
|
|||
"content": "hello there",
|
||||
"attributedTo": track_attributed_to.fid,
|
||||
"album": serializers.AlbumSerializer(track.album).data,
|
||||
"artists": [serializers.ArtistSerializer(track.artist).data],
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
track.artist_credit.all(), many=True
|
||||
).data,
|
||||
"image": {"type": "Image", "mediaType": "image/jpeg", "url": faker.url()},
|
||||
"tag": [
|
||||
{"type": "Hashtag", "name": "#Hello"},
|
||||
|
@ -1213,23 +1291,35 @@ def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
|
|||
"href": "https://cover.image/test.png",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "John Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artistcredit",
|
||||
}
|
||||
],
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"published": published.isoformat(),
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "John Smith",
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artistcredit",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -1259,7 +1349,6 @@ def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
|
|||
def test_activity_pub_upload_serializer_from_ap_update(factories, mocker, now, r_mock):
|
||||
library = factories["music.Library"]()
|
||||
upload = factories["music.Upload"](library=library, track__album__with_cover=True)
|
||||
|
||||
data = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"type": "Audio",
|
||||
|
@ -1631,7 +1720,7 @@ def test_channel_actor_outbox_serializer(factories):
|
|||
channel = factories["audio.Channel"]()
|
||||
uploads = factories["music.Upload"].create_batch(
|
||||
5,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
library=channel.library,
|
||||
import_status="finished",
|
||||
)
|
||||
|
@ -1671,7 +1760,8 @@ def test_channel_upload_serializer(factories):
|
|||
track__license="cc0-1.0",
|
||||
track__copyright="Copyright something",
|
||||
track__album__set_tags=["Rock"],
|
||||
track__artist__set_tags=["Indie"],
|
||||
track__artist_credit__artist__set_tags=["Indie"],
|
||||
track__artist_credit__artist__local=True,
|
||||
)
|
||||
|
||||
expected = {
|
||||
|
@ -1722,9 +1812,9 @@ def test_channel_upload_serializer(factories):
|
|||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_channel_upload_serializer_from_ap_create(factories, now):
|
||||
def test_channel_upload_serializer_from_ap_create(factories, now, mocker):
|
||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"type": "Audio",
|
||||
|
@ -1767,6 +1857,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
|||
"url": "https://image.example/image.png",
|
||||
},
|
||||
}
|
||||
mocker.patch.object(utils, "retrieve_ap_object", return_value=album)
|
||||
|
||||
serializer = serializers.ChannelUploadSerializer(
|
||||
data=payload, context={"channel": channel}
|
||||
|
@ -1784,7 +1875,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
|||
assert upload.size == payload["url"][1]["size"]
|
||||
assert upload.bitrate == payload["url"][1]["bitrate"]
|
||||
assert upload.duration == payload["duration"]
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
assert upload.track.position == payload["position"]
|
||||
assert upload.track.disc_number == payload["disc"]
|
||||
assert upload.track.attributed_to == channel.attributed_to
|
||||
|
@ -1801,10 +1892,12 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
|||
assert upload.track.album == album
|
||||
|
||||
|
||||
def test_channel_upload_serializer_from_ap_update(factories, now):
|
||||
def test_channel_upload_serializer_from_ap_update(factories, now, mocker):
|
||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
upload = factories["music.Upload"](track__album=album, track__artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
upload = factories["music.Upload"](
|
||||
track__album=album, track__artist_credit__artist=channel.artist
|
||||
)
|
||||
|
||||
payload = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
|
@ -1847,6 +1940,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now):
|
|||
"url": "https://image.example/image.png",
|
||||
},
|
||||
}
|
||||
mocker.patch.object(utils, "retrieve_ap_object", return_value=album)
|
||||
|
||||
serializer = serializers.ChannelUploadSerializer(
|
||||
data=payload, context={"channel": channel}
|
||||
|
@ -1865,7 +1959,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now):
|
|||
assert upload.size == payload["url"][1]["size"]
|
||||
assert upload.bitrate == payload["url"][1]["bitrate"]
|
||||
assert upload.duration == payload["duration"]
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
assert upload.track.position == payload["position"]
|
||||
assert upload.track.disc_number == payload["disc"]
|
||||
assert upload.track.attributed_to == channel.attributed_to
|
||||
|
@ -1944,3 +2038,60 @@ def test_report_serializer_to_ap(factories):
|
|||
}
|
||||
serializer = serializers.FlagSerializer(report)
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
def test_artist_credit_serializer_to_ap(factories):
|
||||
ac = factories["music.ArtistCredit"](artist__local=True)
|
||||
serializer = serializers.ArtistCreditSerializer(ac)
|
||||
|
||||
expected = {
|
||||
"@context": jsonld.get_default_context(),
|
||||
"id": ac.get_federation_id(),
|
||||
"type": "ArtistCredit",
|
||||
"artist": serializers.ArtistSerializer(
|
||||
ac.artist, context={"include_ap_context": False}
|
||||
).data,
|
||||
"joinphrase": ac.joinphrase,
|
||||
"name": ac.credit,
|
||||
"index": ac.index,
|
||||
"musicbrainzId": ac.mbid,
|
||||
"published": ac.creation_date.isoformat(),
|
||||
}
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
# def test_artist_credit_serializer_from_ap(factories):
|
||||
# ac = factories["music.ArtistCredit"](artist__local=True)
|
||||
|
||||
# payload = {
|
||||
# "@context": jsonld.get_default_context(),
|
||||
# "id": ac.get_federation_id(),
|
||||
# "type": "ArtistCredit",
|
||||
# "artist": serializers.ArtistSerializer(
|
||||
# ac.artist, context={"include_ap_context": False}
|
||||
# ).data,
|
||||
# "joinphrase": ac.joinphrase,
|
||||
# "name": ac.credit,
|
||||
# "index": ac.index,
|
||||
# "musicbrainzId": ac.mbid,
|
||||
# "published": ac.creation_date.isoformat(),
|
||||
# }
|
||||
|
||||
# expected = {
|
||||
# "id": ac.get_federation_id(),
|
||||
# "type": "ArtistCredit",
|
||||
# "artist": serializers.ArtistSerializer(
|
||||
# ac.artist, context={"include_ap_context": False}
|
||||
# ).data,
|
||||
# "joinphrase": ac.joinphrase,
|
||||
# "name": ac.credit,
|
||||
# "index": ac.index,
|
||||
# "musicbrainzId": ac.mbid,
|
||||
# "published": ac.creation_date.isoformat(),
|
||||
# }
|
||||
# serializer = serializers.ArtistCreditSerializer(data=payload)
|
||||
|
||||
# serializer.is_valid()
|
||||
|
||||
# assert serializer.validated_data == expected
|
||||
|
|
|
@ -4,7 +4,9 @@ from funkwhale_api.history import filters, models
|
|||
def test_listening_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||
factories["history.Listening"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_listening = factories["history.Listening"](track__artist=cf.target_artist)
|
||||
hidden_listening = factories["history.Listening"](
|
||||
track__artist_credit__artist=cf.target_artist
|
||||
)
|
||||
qs = models.Listening.objects.all()
|
||||
filterset = filters.ListeningFilter(
|
||||
{"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
|
||||
|
@ -17,7 +19,7 @@ def test_listening_filter_track_album_artist(factories, mocker, queryset_equal_l
|
|||
factories["history.Listening"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_listening = factories["history.Listening"](
|
||||
track__album__artist=cf.target_artist
|
||||
track__album__artist_credit__artist=cf.target_artist
|
||||
)
|
||||
qs = models.Listening.objects.all()
|
||||
filterset = filters.ListeningFilter(
|
||||
|
|
|
@ -385,7 +385,13 @@ def test_manage_album_serializer(factories, now, to_api_date):
|
|||
"creation_date": to_api_date(album.creation_date),
|
||||
"release_date": album.release_date.isoformat(),
|
||||
"cover": common_serializers.AttachmentSerializer(album.attachment_cover).data,
|
||||
"artist": serializers.ManageNestedArtistSerializer(album.artist).data,
|
||||
"artist_credit": [
|
||||
{
|
||||
"artist": serializers.ManageNestedArtistSerializer(
|
||||
album.artist_credit.all()[0].artist
|
||||
).data
|
||||
}
|
||||
],
|
||||
"attributed_to": serializers.ManageBaseActorSerializer(
|
||||
album.attributed_to
|
||||
).data,
|
||||
|
@ -412,7 +418,13 @@ def test_manage_track_serializer(factories, now, to_api_date):
|
|||
"copyright": track.copyright,
|
||||
"license": track.license,
|
||||
"creation_date": to_api_date(track.creation_date),
|
||||
"artist": serializers.ManageNestedArtistSerializer(track.artist).data,
|
||||
"artist_credit": [
|
||||
{
|
||||
"artist": serializers.ManageNestedArtistSerializer(
|
||||
track.artist_credit.all()[0].artist
|
||||
).data
|
||||
}
|
||||
],
|
||||
"album": serializers.ManageTrackAlbumSerializer(track.album).data,
|
||||
"attributed_to": serializers.ManageBaseActorSerializer(
|
||||
track.attributed_to
|
||||
|
|
|
@ -216,7 +216,9 @@ def test_album_list(factories, superuser_api_client, settings):
|
|||
album = factories["music.Album"]()
|
||||
factories["music.Album"]()
|
||||
url = reverse("api:v1:manage:library:albums-list")
|
||||
response = superuser_api_client.get(url, {"q": f'artist:"{album.artist.name}"'})
|
||||
response = superuser_api_client.get(
|
||||
url, {"q": f'artist:"{album.artist_credit.all()[0].artist.name}"'}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ def test_album_filter_hidden(factories, mocker, queryset_equal_list):
|
|||
factories["music.Album"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
|
||||
hidden_album = factories["music.Album"](artist=cf.target_artist)
|
||||
hidden_album = factories["music.Album"](artist_credit__artist=cf.target_artist)
|
||||
|
||||
qs = models.Album.objects.all()
|
||||
filterset = filters.AlbumFilter(
|
||||
|
@ -55,7 +55,7 @@ def test_artist_filter_hidden(factories, mocker, queryset_equal_list):
|
|||
def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||
factories["music.Track"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_track = factories["music.Track"](artist=cf.target_artist)
|
||||
hidden_track = factories["music.Track"](artist_credit__artist=cf.target_artist)
|
||||
|
||||
qs = models.Track.objects.all()
|
||||
filterset = filters.TrackFilter(
|
||||
|
@ -68,7 +68,9 @@ def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
|
|||
def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list):
|
||||
factories["music.Track"]()
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
hidden_track = factories["music.Track"](album__artist=cf.target_artist)
|
||||
hidden_track = factories["music.Track"](
|
||||
album__artist_credit__artist=cf.target_artist
|
||||
)
|
||||
|
||||
qs = models.Track.objects.all()
|
||||
filterset = filters.TrackFilter(
|
||||
|
@ -137,7 +139,9 @@ def test_track_filter_tag_multiple(
|
|||
def test_channel_filter_track(factories, queryset_equal_list, mocker, anonymous_user):
|
||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||
upload = factories["music.Upload"](
|
||||
library=channel.library, playable=True, track__artist=channel.artist
|
||||
library=channel.library,
|
||||
playable=True,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
)
|
||||
factories["music.Track"]()
|
||||
qs = upload.track.__class__.objects.all()
|
||||
|
@ -153,7 +157,9 @@ def test_channel_filter_track(factories, queryset_equal_list, mocker, anonymous_
|
|||
def test_channel_filter_album(factories, queryset_equal_list, mocker, anonymous_user):
|
||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||
upload = factories["music.Upload"](
|
||||
library=channel.library, playable=True, track__artist=channel.artist
|
||||
library=channel.library,
|
||||
playable=True,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
)
|
||||
factories["music.Album"]()
|
||||
qs = upload.track.album.__class__.objects.all()
|
||||
|
@ -198,14 +204,14 @@ def test_library_filter_artist(factories, queryset_equal_list, mocker, anonymous
|
|||
library = factories["music.Library"](privacy_level="everyone")
|
||||
upload = factories["music.Upload"](library=library, playable=True)
|
||||
factories["music.Artist"]()
|
||||
qs = upload.track.artist.__class__.objects.all()
|
||||
|
||||
qs = models.Artist.objects.all()
|
||||
filterset = filters.ArtistFilter(
|
||||
{"library": library.uuid},
|
||||
request=mocker.Mock(user=anonymous_user, actor=None),
|
||||
queryset=qs,
|
||||
)
|
||||
|
||||
assert filterset.qs == [upload.track.artist]
|
||||
assert filterset.qs == [upload.track.artist_credit.all()[0].artist]
|
||||
|
||||
|
||||
def test_track_filter_artist_includes_album_artist(
|
||||
|
@ -214,12 +220,13 @@ def test_track_filter_artist_includes_album_artist(
|
|||
factories["music.Track"]()
|
||||
track1 = factories["music.Track"]()
|
||||
track2 = factories["music.Track"](
|
||||
album__artist=track1.artist, artist=factories["music.Artist"]()
|
||||
album__artist_credit__artist=track1.artist_credit.all()[0].artist,
|
||||
artist_credit__artist=factories["music.Artist"](),
|
||||
)
|
||||
|
||||
qs = models.Track.objects.all()
|
||||
filterset = filters.TrackFilter(
|
||||
{"artist": track1.artist.pk},
|
||||
{"artist": track1.artist_credit.all()[0].artist.pk},
|
||||
request=mocker.Mock(user=anonymous_user),
|
||||
queryset=qs,
|
||||
)
|
||||
|
@ -263,3 +270,28 @@ def test_filter_tag_related(
|
|||
queryset=obj.__class__.objects.all(),
|
||||
)
|
||||
assert filterset.qs == matches
|
||||
|
||||
|
||||
# def test_artist_credit_filter(factories, mocker, queryset_equal_list, anonymous_user):
|
||||
# ac = factories["music.ArtistCredit"]()
|
||||
# factories["music.Track"]()
|
||||
# track1 = factories["music.Track"](artist_credit=ac)
|
||||
# track2 = factories["music.Track"](album__artist_credit__artist=ac.artist)
|
||||
|
||||
# # qs = models.ArtistCredit.objects.all()
|
||||
# # filterset = filters.ArtistCreditFilter(
|
||||
# # {"q": ac.credit},
|
||||
# # request=mocker.Mock(user=anonymous_user),
|
||||
# # queryset=qs,
|
||||
# # )
|
||||
|
||||
# # for acf in filterset.qs:
|
||||
# # assert acf.credit == ac.credit
|
||||
|
||||
# qs = models.Artist.objects.all()
|
||||
# filterset = filters.TrackFilter(
|
||||
# {"artist": ac.artist.pk},
|
||||
# # {"playable": True},
|
||||
# request=mocker.Mock(user=anonymous_user, actor=None),
|
||||
# )
|
||||
# assert filterset.qs == [track1, track2]
|
||||
|
|
|
@ -316,8 +316,13 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Binärpilot",
|
||||
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Another artist",
|
||||
"mbid": None,
|
||||
"joinphrase": "",
|
||||
},
|
||||
{"name": "Another artist", "mbid": None},
|
||||
],
|
||||
"album": {
|
||||
"title": "You Can't Stop Da Funk",
|
||||
|
@ -327,8 +332,9 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Binärpilot",
|
||||
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{"name": "Another artist", "mbid": None},
|
||||
{"name": "Another artist", "mbid": None, "joinphrase": ""},
|
||||
],
|
||||
},
|
||||
"position": 2,
|
||||
|
@ -348,8 +354,13 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
"mbid": None,
|
||||
"joinphrase": "",
|
||||
},
|
||||
{"name": "Musopen Symphony Orchestra", "mbid": None},
|
||||
],
|
||||
"album": {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||
|
@ -359,10 +370,12 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||
"joinphrase": "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -383,8 +396,13 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
"mbid": None,
|
||||
"joinphrase": "",
|
||||
},
|
||||
{"name": "Musopen Symphony Orchestra", "mbid": None},
|
||||
],
|
||||
"album": {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||
|
@ -394,10 +412,12 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||
"joinphrase": "",
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -418,6 +438,7 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Die Toten Hosen",
|
||||
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
||||
"joinphrase": "",
|
||||
}
|
||||
],
|
||||
"album": {
|
||||
|
@ -428,6 +449,7 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Die Toten Hosen",
|
||||
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
||||
"joinphrase": "",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -450,6 +472,7 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Nine Inch Nails",
|
||||
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
||||
"joinphrase": "",
|
||||
}
|
||||
],
|
||||
"album": {
|
||||
|
@ -460,6 +483,7 @@ def test_metadata_fallback_ogg_theora(mocker):
|
|||
{
|
||||
"name": "Nine Inch Nails",
|
||||
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
||||
"joinphrase": "",
|
||||
}
|
||||
],
|
||||
},
|
||||
|
@ -499,10 +523,12 @@ def test_track_metadata_serializer(path, expected, mocker):
|
|||
{
|
||||
"name": "Hello",
|
||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "World",
|
||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
||||
"joinphrase": "",
|
||||
},
|
||||
],
|
||||
),
|
||||
|
@ -515,12 +541,14 @@ def test_track_metadata_serializer(path, expected, mocker):
|
|||
{
|
||||
"name": "Hello",
|
||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "World",
|
||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{"name": "Foo", "mbid": None},
|
||||
{"name": "Foo", "mbid": None, "joinphrase": ""},
|
||||
],
|
||||
),
|
||||
],
|
||||
|
@ -588,6 +616,7 @@ def test_fake_metadata_with_serializer():
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "",
|
||||
}
|
||||
],
|
||||
"album": {
|
||||
|
@ -598,10 +627,12 @@ def test_fake_metadata_with_serializer():
|
|||
{
|
||||
"name": "Edvard Grieg",
|
||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||
"joinphrase": "; ",
|
||||
},
|
||||
{
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||
"joinphrase": "",
|
||||
},
|
||||
],
|
||||
"cover_data": None,
|
||||
|
@ -626,7 +657,7 @@ def test_serializer_album_artist_missing():
|
|||
|
||||
expected = {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||
"artists": [{"name": "Edvard Grieg", "mbid": None}],
|
||||
"artists": [{"name": "Edvard Grieg", "mbid": None, "joinphrase": ""}],
|
||||
"album": {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||
"mbid": None,
|
||||
|
@ -654,7 +685,7 @@ def test_serializer_album_artist_missing():
|
|||
def test_serializer_album_default_title_when_missing_or_empty(data):
|
||||
expected = {
|
||||
"title": "Track",
|
||||
"artists": [{"name": "Artist", "mbid": None}],
|
||||
"artists": [{"name": "Artist", "mbid": None, "joinphrase": ""}],
|
||||
"album": {
|
||||
"title": metadata.UNKNOWN_ALBUM,
|
||||
"mbid": None,
|
||||
|
@ -681,7 +712,7 @@ def test_serializer_empty_fields(field_name):
|
|||
}
|
||||
expected = {
|
||||
"title": "Track Title",
|
||||
"artists": [{"name": "Track Artist", "mbid": None}],
|
||||
"artists": [{"name": "Track Artist", "mbid": None, "joinphrase": ""}],
|
||||
"album": {
|
||||
"title": "Track Album",
|
||||
"mbid": None,
|
||||
|
@ -698,7 +729,7 @@ def test_serializer_empty_fields(field_name):
|
|||
def test_serializer_strict_mode_false():
|
||||
data = {}
|
||||
expected = {
|
||||
"artists": [{"name": None, "mbid": None}],
|
||||
"artists": [],
|
||||
"album": {
|
||||
"title": "[Unknown Album]",
|
||||
"mbid": None,
|
||||
|
@ -719,6 +750,8 @@ def test_serializer_strict_mode_true():
|
|||
serializer = metadata.TrackMetadataSerializer(
|
||||
data=metadata.FakeMetadata(data), context={"strict": True}
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception=True)
|
||||
with pytest.raises(metadata.serializers.ValidationError):
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
|
||||
|
@ -730,11 +763,21 @@ def test_artist_field_featuring():
|
|||
"musicbrainz_artistid": "9a3bf45c-347d-4630-894d-7cf3e8e0b632/cbf9738d-8f81-4a92-bc64-ede09341652d",
|
||||
}
|
||||
|
||||
expected = [{"name": "Santana feat. Chris Cornell", "mbid": None}]
|
||||
expected = [
|
||||
{
|
||||
"name": "Santana",
|
||||
"mbid": uuid.UUID("9a3bf45c-347d-4630-894d-7cf3e8e0b632"),
|
||||
"joinphrase": " feat. ",
|
||||
},
|
||||
{
|
||||
"name": "Chris Cornell",
|
||||
"mbid": uuid.UUID("cbf9738d-8f81-4a92-bc64-ede09341652d"),
|
||||
"joinphrase": "",
|
||||
},
|
||||
]
|
||||
|
||||
field = metadata.ArtistField()
|
||||
value = field.get_value(data)
|
||||
|
||||
assert field.to_internal_value(value) == expected
|
||||
|
||||
|
||||
|
@ -761,7 +804,7 @@ def test_acquire_tags_from_genre(genre, expected_tags):
|
|||
}
|
||||
expected = {
|
||||
"title": "Track Title",
|
||||
"artists": [{"name": "Track Artist", "mbid": None}],
|
||||
"artists": [{"name": "Track Artist", "mbid": None, "joinphrase": ""}],
|
||||
"album": {
|
||||
"title": "Track Album",
|
||||
"mbid": None,
|
||||
|
|
|
@ -43,7 +43,7 @@ def test_import_album_stores_release_group(factories):
|
|||
album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[])
|
||||
|
||||
assert album.release_group_id == album_data["release-group"]["id"]
|
||||
assert album.artist == artist
|
||||
assert album.artist_credit.all()[0].artist == artist
|
||||
|
||||
|
||||
def test_import_track_from_release(factories, mocker):
|
||||
|
@ -91,8 +91,72 @@ def test_import_track_from_release(factories, mocker):
|
|||
mocked_get.assert_called_once_with(album.mbid, includes=models.Album.api_includes)
|
||||
assert track.title == track_data["recording"]["title"]
|
||||
assert track.mbid == track_data["recording"]["id"]
|
||||
assert track.album == album
|
||||
assert track.artist == artist
|
||||
assert track.artist_credit.all()[0].albums.all()[0] == album
|
||||
assert track.artist_credit.all()[0].artist == artist
|
||||
assert track.position == int(track_data["position"])
|
||||
|
||||
|
||||
def test_import_track_from_multi_artist_credit_release(factories, mocker):
|
||||
album = factories["music.Album"](mbid="430347cb-0879-3113-9fde-c75b658c298e")
|
||||
artist = factories["music.Artist"](mbid="a5211c65-2465-406b-93ec-213588869dc1")
|
||||
artist2 = factories["music.Artist"](mbid="a5211c65-2465-406b-93ec-21358ee69dc1")
|
||||
album_data = {
|
||||
"release": {
|
||||
"id": album.mbid,
|
||||
"title": "Daydream Nation",
|
||||
"status": "Official",
|
||||
"medium-count": 1,
|
||||
"medium-list": [
|
||||
{
|
||||
"position": "1",
|
||||
"format": "CD",
|
||||
"track-list": [
|
||||
{
|
||||
"id": "03baca8b-855a-3c05-8f3d-d3235287d84d",
|
||||
"position": "4",
|
||||
"number": "4",
|
||||
"length": "417973",
|
||||
"recording": {
|
||||
"id": "2109e376-132b-40ad-b993-2bb6812e19d4",
|
||||
"title": "Teen Age Riot",
|
||||
"length": "417973",
|
||||
"artist-credit": [
|
||||
{
|
||||
"joinphrase": "feat",
|
||||
"artist": {
|
||||
"id": artist.mbid,
|
||||
"name": artist.name,
|
||||
},
|
||||
},
|
||||
{
|
||||
"joinphrase": "",
|
||||
"artist": {
|
||||
"id": artist2.mbid,
|
||||
"name": artist2.name,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"track_or_recording_length": "417973",
|
||||
}
|
||||
],
|
||||
"track-count": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
}
|
||||
mocked_get = mocker.patch(
|
||||
"funkwhale_api.musicbrainz.api.releases.get", return_value=album_data
|
||||
)
|
||||
track_data = album_data["release"]["medium-list"][0]["track-list"][0]
|
||||
track = models.Track.get_or_create_from_release(
|
||||
"430347cb-0879-3113-9fde-c75b658c298e", track_data["recording"]["id"]
|
||||
)[0]
|
||||
mocked_get.assert_called_once_with(album.mbid, includes=models.Album.api_includes)
|
||||
assert track.title == track_data["recording"]["title"]
|
||||
assert track.mbid == track_data["recording"]["id"]
|
||||
assert track.artist_credit.all()[0].albums.all()[0] == album
|
||||
assert [ac.artist for ac in track.artist_credit.all()] == [artist, artist2]
|
||||
assert track.position == int(track_data["position"])
|
||||
|
||||
|
||||
|
@ -144,12 +208,11 @@ def test_import_track_with_different_artist_than_release(factories, mocker):
|
|||
mocker.patch(
|
||||
"funkwhale_api.musicbrainz.api.recordings.get", return_value=recording_data
|
||||
)
|
||||
|
||||
track = models.Track.get_or_create_from_api(recording_data["recording"]["id"])[0]
|
||||
assert track.title == recording_data["recording"]["title"]
|
||||
assert track.mbid == recording_data["recording"]["id"]
|
||||
assert track.album == album
|
||||
assert track.artist == artist
|
||||
assert track.artist_credit.all()[0].artist == artist
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
@ -391,7 +454,7 @@ def test_artist_playable_by_correct_actor(privacy_level, expected, factories):
|
|||
queryset = models.Artist.objects.playable_by(
|
||||
upload.library.actor
|
||||
).annotate_playable_by_actor(upload.library.actor)
|
||||
match = upload.track.artist in list(queryset)
|
||||
match = [ac.artist for ac in upload.track.artist_credit.all()][0] == queryset.get()
|
||||
assert match is expected
|
||||
if expected:
|
||||
assert bool(queryset.first().is_playable_by_actor) is expected
|
||||
|
@ -410,7 +473,7 @@ def test_artist_playable_by_instance_actor(privacy_level, expected, factories):
|
|||
queryset = models.Artist.objects.playable_by(
|
||||
instance_actor
|
||||
).annotate_playable_by_actor(instance_actor)
|
||||
match = upload.track.artist in list(queryset)
|
||||
match = [ac.artist for ac in upload.track.artist_credit.all()][0] in queryset
|
||||
assert match is expected
|
||||
if expected:
|
||||
assert bool(queryset.first().is_playable_by_actor) is expected
|
||||
|
@ -426,7 +489,7 @@ def test_artist_playable_by_anonymous(privacy_level, expected, factories):
|
|||
library__local=True,
|
||||
)
|
||||
queryset = models.Artist.objects.playable_by(None).annotate_playable_by_actor(None)
|
||||
match = upload.track.artist in list(queryset)
|
||||
match = [ac.artist for ac in upload.track.artist_credit.all()][0] in queryset
|
||||
assert match is expected
|
||||
if expected:
|
||||
assert bool(queryset.first().is_playable_by_actor) is expected
|
||||
|
|
|
@ -39,8 +39,10 @@ def test_can_create_album_from_api(artists, albums, mocker, db):
|
|||
assert album.mbid, data["id"]
|
||||
assert album.title, "Hypnotize"
|
||||
assert album.release_date, datetime.date(2005, 1, 1)
|
||||
assert album.artist.name, "System of a Down"
|
||||
assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"]
|
||||
assert album.artist_credit.all()[0].artist.name, "System of a Down"
|
||||
assert album.artist_credit.all()[0].artist.mbid, data["artist-credit"][0]["artist"][
|
||||
"id"
|
||||
]
|
||||
assert album.fid == federation_utils.full_url(
|
||||
f"/federation/music/albums/{album.uuid}"
|
||||
)
|
||||
|
@ -64,9 +66,12 @@ def test_can_create_track_from_api(artists, albums, tracks, mocker, db):
|
|||
assert int(data["ext:score"]) == 100
|
||||
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
||||
assert track.mbid == data["id"]
|
||||
assert track.artist.pk is not None
|
||||
assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
|
||||
assert track.artist.name == "Adhesive Wombat"
|
||||
assert track.artist_credit.all()[0].artist.pk is not None
|
||||
assert (
|
||||
str(track.artist_credit.all()[0].artist.mbid)
|
||||
== "62c3befb-6366-4585-b256-809472333801"
|
||||
)
|
||||
assert track.artist_credit.all()[0].artist.name == "Adhesive Wombat"
|
||||
assert str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e"
|
||||
assert track.album.title == "Marsupial Madness"
|
||||
assert track.fid == federation_utils.full_url(
|
||||
|
@ -114,9 +119,12 @@ def test_can_get_or_create_track_from_api(artists, albums, tracks, mocker, db):
|
|||
assert int(data["ext:score"]) == 100
|
||||
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
||||
assert track.mbid == data["id"]
|
||||
assert track.artist.pk is not None
|
||||
assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
|
||||
assert track.artist.name == "Adhesive Wombat"
|
||||
assert track.artist_credit.all()[0].artist.pk is not None
|
||||
assert (
|
||||
str(track.artist_credit.all()[0].artist.mbid)
|
||||
== "62c3befb-6366-4585-b256-809472333801"
|
||||
)
|
||||
assert track.artist_credit.all()[0].artist.name == "Adhesive Wombat"
|
||||
|
||||
track2, created = models.Track.get_or_create_from_api(mbid=data["id"])
|
||||
assert not created
|
||||
|
|
|
@ -38,7 +38,7 @@ def test_artist_album_serializer(factories, to_api_date):
|
|||
"fid": album.fid,
|
||||
"mbid": str(album.mbid),
|
||||
"title": album.title,
|
||||
"artist": album.artist.id,
|
||||
"artist_credit": [ac.id for ac in album.artist_credit.all()],
|
||||
"creation_date": to_api_date(album.creation_date),
|
||||
"tracks_count": 1,
|
||||
"is_playable": None,
|
||||
|
@ -53,12 +53,14 @@ def test_artist_album_serializer(factories, to_api_date):
|
|||
|
||||
def test_artist_with_albums_serializer(factories, to_api_date):
|
||||
actor = factories["federation.Actor"]()
|
||||
track = factories["music.Track"](
|
||||
album__artist__attributed_to=actor, album__artist__with_cover=True
|
||||
artist_credit = factories["music.ArtistCredit"](
|
||||
artist__attributed_to=actor, artist__with_cover=True
|
||||
)
|
||||
artist = track.artist
|
||||
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
||||
album = list(artist.albums.all())[0]
|
||||
|
||||
track = factories["music.Track"](album__artist_credit=artist_credit)
|
||||
artist = track.artist_credit.all()[0].artist
|
||||
# artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
||||
album = artist.artist_credit.all()[0].albums.all()[0]
|
||||
setattr(artist, "_prefetched_tracks", range(42))
|
||||
expected = {
|
||||
"id": artist.id,
|
||||
|
@ -82,10 +84,11 @@ def test_artist_with_albums_serializer(factories, to_api_date):
|
|||
def test_artist_with_albums_serializer_channel(factories, to_api_date):
|
||||
actor = factories["federation.Actor"]()
|
||||
channel = factories["audio.Channel"](attributed_to=actor, artist__with_cover=True)
|
||||
track = factories["music.Track"](album__artist=channel.artist)
|
||||
artist = track.artist
|
||||
artist_credit = factories["music.ArtistCredit"](artist=channel.artist)
|
||||
track = factories["music.Track"](album__artist_credit=artist_credit)
|
||||
artist = track.artist_credit.all()[0].artist
|
||||
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
||||
album = list(artist.albums.all())[0]
|
||||
album = list(artist.artist_credit.all()[0].albums.all())[0]
|
||||
setattr(artist, "_prefetched_tracks", range(42))
|
||||
expected = {
|
||||
"id": artist.id,
|
||||
|
@ -177,7 +180,9 @@ def test_album_serializer(factories, to_api_date):
|
|||
"fid": album.fid,
|
||||
"mbid": str(album.mbid),
|
||||
"title": album.title,
|
||||
"artist": serializers.SimpleArtistSerializer(album.artist).data,
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
album.artist_credit.all(), many=True
|
||||
).data,
|
||||
"creation_date": to_api_date(album.creation_date),
|
||||
"is_playable": False,
|
||||
"duration": 0,
|
||||
|
@ -207,7 +212,9 @@ def test_track_album_serializer(factories, to_api_date):
|
|||
"fid": album.fid,
|
||||
"mbid": str(album.mbid),
|
||||
"title": album.title,
|
||||
"artist": serializers.SimpleArtistSerializer(album.artist).data,
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
album.artist_credit.all(), many=True
|
||||
).data,
|
||||
"creation_date": to_api_date(album.creation_date),
|
||||
"is_playable": False,
|
||||
"cover": common_serializers.AttachmentSerializer(album.attachment_cover).data,
|
||||
|
@ -239,7 +246,9 @@ def test_track_serializer(factories, to_api_date):
|
|||
expected = {
|
||||
"id": track.id,
|
||||
"fid": track.fid,
|
||||
"artist": serializers.SimpleArtistSerializer(track.artist).data,
|
||||
"artist_credit": serializers.ArtistCreditSerializer(
|
||||
track.artist_credit.all(), many=True
|
||||
).data,
|
||||
"album": serializers.TrackAlbumSerializer(track.album).data,
|
||||
"mbid": str(track.mbid),
|
||||
"title": track.title,
|
||||
|
|
|
@ -41,7 +41,10 @@ def test_library_track(spa_html, no_api_auth, client, factories, settings):
|
|||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": track.artist.pk}),
|
||||
utils.spa_reverse(
|
||||
"library_artist",
|
||||
kwargs={"pk": track.artist_credit.all()[0].artist.pk},
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -117,7 +120,10 @@ def test_library_album(spa_html, no_api_auth, client, factories, settings):
|
|||
"property": "music:musician",
|
||||
"content": utils.join_url(
|
||||
settings.FUNKWHALE_URL,
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": album.artist.pk}),
|
||||
utils.spa_reverse(
|
||||
"library_artist",
|
||||
kwargs={"pk": album.artist_credit.all()[0].artist.pk},
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -166,7 +172,7 @@ def test_library_album(spa_html, no_api_auth, client, factories, settings):
|
|||
def test_library_artist(spa_html, no_api_auth, client, factories, settings):
|
||||
album = factories["music.Album"](with_cover=True)
|
||||
factories["music.Upload"](playable=True, track__album=album)
|
||||
artist = album.artist
|
||||
artist = album.artist_credit.all()[0].artist
|
||||
url = f"/library/artists/{artist.pk}"
|
||||
|
||||
response = client.get(url)
|
||||
|
|
|
@ -4,6 +4,7 @@ import uuid
|
|||
|
||||
import pytest
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from funkwhale_api.common import utils as common_utils
|
||||
|
@ -43,12 +44,12 @@ def test_can_create_track_from_file_metadata_no_mbid(db, mocker):
|
|||
assert track.album.title == metadata["album"]["title"]
|
||||
assert track.album.mbid is None
|
||||
assert track.album.release_date == datetime.date(2012, 8, 15)
|
||||
assert track.artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist.mbid is None
|
||||
assert track.artist.attributed_to is None
|
||||
assert track.artist_credit.all()[0].artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist_credit.all()[0].artist.mbid is None
|
||||
assert track.artist_credit.all()[0].artist.attributed_to is None
|
||||
match_license.assert_called_once_with(metadata["license"], metadata["copyright"])
|
||||
add_tags.assert_any_call(track, *metadata["tags"])
|
||||
add_tags.assert_any_call(track.artist, *[])
|
||||
add_tags.assert_any_call(track.artist_credit.all()[0].artist, *[])
|
||||
add_tags.assert_any_call(track.album, *[])
|
||||
|
||||
|
||||
|
@ -75,12 +76,12 @@ def test_can_create_track_from_file_metadata_attributed_to(factories, mocker):
|
|||
assert track.album.mbid is None
|
||||
assert track.album.release_date == datetime.date(2012, 8, 15)
|
||||
assert track.album.attributed_to == actor
|
||||
assert track.artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist.mbid is None
|
||||
assert track.artist.attributed_to == actor
|
||||
assert track.artist_credit.all()[0].artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist_credit.all()[0].artist.mbid is None
|
||||
assert track.artist_credit.all()[0].artist.attributed_to == actor
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_featuring(factories):
|
||||
def test_can_create_track_from_file_metadata_featuring(mocker):
|
||||
metadata = {
|
||||
"title": "Whole Lotta Love",
|
||||
"position": 1,
|
||||
|
@ -94,12 +95,47 @@ def test_can_create_track_from_file_metadata_featuring(factories):
|
|||
{"name": "Santana", "mbid": "9a3bf45c-347d-4630-894d-7cf3e8e0b632"}
|
||||
],
|
||||
},
|
||||
"artists": [{"name": "Santana feat. Chris Cornell", "mbid": None}],
|
||||
"artists": [{"name": "Santana feat Chris Cornell", "mbid": None}],
|
||||
}
|
||||
mb_ac = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "9a3bf45c-347d-4630-894d-7cf3e8e0b632",
|
||||
"name": "Santana",
|
||||
},
|
||||
"joinphrase": " feat ",
|
||||
"name": "Santana",
|
||||
},
|
||||
{
|
||||
"artist": {
|
||||
"name": "Chris Cornell",
|
||||
"id": "9a3bf45c-347d-4630-894d-7cf3e8e0acab",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Chris Cornell",
|
||||
},
|
||||
]
|
||||
}
|
||||
mb_ac_album = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "9a3bf45c-347d-4630-894d-7cf3e8e0b632",
|
||||
"name": "Santana",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Santana",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mocker.patch.object(tasks.musicbrainz.api.recordings, "get", return_value=mb_ac)
|
||||
mocker.patch.object(tasks.musicbrainz.api.releases, "get", return_value=mb_ac_album)
|
||||
track = tasks.get_track_from_import_metadata(metadata)
|
||||
|
||||
assert track.album.artist.name == "Santana"
|
||||
assert track.artist.name == "Santana feat. Chris Cornell"
|
||||
assert track.album.artist_credit.all()[0].artist.name == "Santana"
|
||||
assert track.get_artist_credit_string == "Santana feat Chris Cornell"
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_description(factories):
|
||||
|
@ -128,7 +164,7 @@ def test_can_create_track_from_file_metadata_use_featuring(factories):
|
|||
}
|
||||
track = tasks.get_track_from_import_metadata(metadata)
|
||||
|
||||
assert track.artist.name == "Anatnas"
|
||||
assert track.get_artist_credit_string == "Santana;Anatnas"
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_mbid(factories, mocker):
|
||||
|
@ -153,6 +189,34 @@ def test_can_create_track_from_file_metadata_mbid(factories, mocker):
|
|||
"cover_data": {"content": b"image_content", "mimetype": "image/png"},
|
||||
}
|
||||
|
||||
mb_ac = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "9c6bddde-6228-4d9f-ad0d-03f6fcb19e13",
|
||||
"name": "Test artist",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Test artist",
|
||||
}
|
||||
]
|
||||
}
|
||||
mb_ac_album = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "9c6bddde-6478-4d9f-ad0d-03f6fcb19e13",
|
||||
"name": "Test album artist",
|
||||
},
|
||||
"joinphrase": "; ",
|
||||
"name": "Test album artist",
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
mocker.patch.object(tasks.musicbrainz.api.recordings, "get", return_value=mb_ac)
|
||||
mocker.patch.object(tasks.musicbrainz.api.releases, "get", return_value=mb_ac_album)
|
||||
|
||||
track = tasks.get_track_from_import_metadata(metadata)
|
||||
|
||||
assert track.title == metadata["title"]
|
||||
|
@ -161,29 +225,64 @@ def test_can_create_track_from_file_metadata_mbid(factories, mocker):
|
|||
assert track.disc_number is None
|
||||
assert track.album.title == metadata["album"]["title"]
|
||||
assert track.album.mbid == metadata["album"]["mbid"]
|
||||
assert track.album.artist.mbid == metadata["album"]["artists"][0]["mbid"]
|
||||
assert track.album.artist.name == metadata["album"]["artists"][0]["name"]
|
||||
assert (
|
||||
str(track.album.artist_credit.all()[0].artist.mbid)
|
||||
== metadata["album"]["artists"][0]["mbid"]
|
||||
)
|
||||
assert (
|
||||
track.album.artist_credit.all()[0].artist.name
|
||||
== metadata["album"]["artists"][0]["name"]
|
||||
)
|
||||
assert track.album.release_date == datetime.date(2012, 8, 15)
|
||||
assert track.artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist.mbid == metadata["artists"][0]["mbid"]
|
||||
assert track.artist_credit.all()[0].artist.name == metadata["artists"][0]["name"]
|
||||
assert (
|
||||
str(track.artist_credit.all()[0].artist.mbid) == metadata["artists"][0]["mbid"]
|
||||
)
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_mbid_existing_album_artist(
|
||||
factories, mocker
|
||||
):
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"]()
|
||||
artist_credit = factories["music.ArtistCredit"](joinphrase="", index=0)
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
metadata = {
|
||||
"album": {
|
||||
"mbid": album.mbid,
|
||||
"title": "",
|
||||
"artists": [{"name": "", "mbid": album.artist.mbid}],
|
||||
"artists": [{"name": "", "mbid": album.artist_credit.all()[0].mbid}],
|
||||
},
|
||||
"title": "Hello",
|
||||
"position": 4,
|
||||
"artists": [{"mbid": artist.mbid, "name": ""}],
|
||||
"mbid": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcb",
|
||||
"artists": [{"mbid": album.artist_credit.all()[0].mbid, "name": ""}],
|
||||
"mbid": "f269d497-1cc0-4ae4-a0c4-157ec7d73acb",
|
||||
}
|
||||
mb_ac_album = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": artist_credit.artist.mbid,
|
||||
"name": artist_credit.artist.name,
|
||||
},
|
||||
"joinphrase": "; ",
|
||||
"name": artist_credit.artist.name,
|
||||
}
|
||||
]
|
||||
}
|
||||
mb_ac = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": album.artist_credit.all()[0].artist.mbid,
|
||||
"name": "Test artist",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": album.artist_credit.all()[0].artist.name,
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mocker.patch.object(tasks.musicbrainz.api.recordings, "get", return_value=mb_ac)
|
||||
mocker.patch.object(tasks.musicbrainz.api.releases, "get", return_value=mb_ac_album)
|
||||
|
||||
track = tasks.get_track_from_import_metadata(metadata)
|
||||
|
||||
|
@ -191,7 +290,7 @@ def test_can_create_track_from_file_metadata_mbid_existing_album_artist(
|
|||
assert track.mbid == metadata["mbid"]
|
||||
assert track.position == 4
|
||||
assert track.album == album
|
||||
assert track.artist == artist
|
||||
assert track.artist_credit.all()[0] == artist_credit
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_fid_existing_album_artist(
|
||||
|
@ -204,7 +303,7 @@ def test_can_create_track_from_file_metadata_fid_existing_album_artist(
|
|||
"album": {
|
||||
"title": "",
|
||||
"fid": album.fid,
|
||||
"artists": [{"name": "", "fid": album.artist.fid}],
|
||||
"artists": [{"name": "", "fid": album.artist_credit.all()[0].artist.fid}],
|
||||
},
|
||||
"title": "Hello",
|
||||
"position": 4,
|
||||
|
@ -217,16 +316,18 @@ def test_can_create_track_from_file_metadata_fid_existing_album_artist(
|
|||
assert track.fid == metadata["fid"]
|
||||
assert track.position == 4
|
||||
assert track.album == album
|
||||
assert track.artist == artist
|
||||
assert track.artist_credit.all()[0].artist == artist
|
||||
|
||||
|
||||
def test_can_create_track_from_file_metadata_distinct_release_mbid(factories):
|
||||
"""Cf https://dev.funkwhale.audio/funkwhale/funkwhale/issues/772"""
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
track = factories["music.Track"](album=album, artist=artist)
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
track = factories["music.Track"](album=album, artist_credit=artist_credit)
|
||||
metadata = {
|
||||
"artists": [{"name": artist.name, "mbid": artist.mbid}],
|
||||
"artists": [
|
||||
{"name": artist_credit.artist.name, "mbid": artist_credit.artist.mbid}
|
||||
],
|
||||
"album": {"title": album.title, "mbid": str(uuid.uuid4())},
|
||||
"title": track.title,
|
||||
"position": 4,
|
||||
|
@ -243,11 +344,13 @@ def test_can_create_track_from_file_metadata_distinct_release_mbid(factories):
|
|||
|
||||
def test_can_create_track_from_file_metadata_distinct_position(factories):
|
||||
"""Cf https://dev.funkwhale.audio/funkwhale/funkwhale/issues/740"""
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
track = factories["music.Track"](album=album, artist=artist)
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
track = factories["music.Track"](album=album, artist_credit=artist_credit)
|
||||
metadata = {
|
||||
"artists": [{"name": artist.name, "mbid": artist.mbid}],
|
||||
"artists": [
|
||||
{"name": artist_credit.artist.name, "mbid": artist_credit.artist.mbid}
|
||||
],
|
||||
"album": {"title": album.title, "mbid": album.mbid},
|
||||
"title": track.title,
|
||||
"position": track.position + 1,
|
||||
|
@ -297,12 +400,24 @@ def test_can_create_track_from_file_metadata_federation(factories, mocker):
|
|||
assert track.album.fid == metadata["album"]["fid"]
|
||||
assert track.album.title == metadata["album"]["title"]
|
||||
assert track.album.creation_date == metadata["album"]["fdate"]
|
||||
assert track.album.artist.fid == metadata["album"]["artists"][0]["fid"]
|
||||
assert track.album.artist.name == metadata["album"]["artists"][0]["name"]
|
||||
assert track.album.artist.creation_date == metadata["album"]["artists"][0]["fdate"]
|
||||
assert track.artist.fid == metadata["artists"][0]["fid"]
|
||||
assert track.artist.name == metadata["artists"][0]["name"]
|
||||
assert track.artist.creation_date == metadata["artists"][0]["fdate"]
|
||||
assert (
|
||||
track.album.artist_credit.all()[0].artist.fid
|
||||
== metadata["album"]["artists"][0]["fid"]
|
||||
)
|
||||
assert (
|
||||
track.album.artist_credit.all()[0].artist.name
|
||||
== metadata["album"]["artists"][0]["name"]
|
||||
)
|
||||
assert (
|
||||
track.album.artist_credit.all()[0].artist.creation_date
|
||||
== metadata["album"]["artists"][0]["fdate"]
|
||||
)
|
||||
assert track.artist_credit.all()[0].artist.fid == metadata["artists"][0]["fid"]
|
||||
assert track.artist_credit.all()[0].artist.name == metadata["artists"][0]["name"]
|
||||
assert (
|
||||
track.artist_credit.all()[0].artist.creation_date
|
||||
== metadata["artists"][0]["fdate"]
|
||||
)
|
||||
|
||||
|
||||
def test_sort_candidates(factories):
|
||||
|
@ -616,22 +731,28 @@ def test_federation_audio_track_to_metadata(now, mocker):
|
|||
"attributedTo": "http://album.attributed",
|
||||
"content": "album desc",
|
||||
"mediaType": "text/plain",
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"type": "Artist",
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"content": "album artist desc",
|
||||
"mediaType": "text/markdown",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": "http://album-artist.attributed",
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||
"image": {
|
||||
"type": "Link",
|
||||
"href": "http://cover.test/album-artist",
|
||||
"mediaType": "image/png",
|
||||
"artist": {
|
||||
"type": "Artist",
|
||||
"published": published.isoformat(),
|
||||
"id": "http://hello.artist",
|
||||
"name": "John Smith",
|
||||
"content": "album artist desc",
|
||||
"mediaType": "text/markdown",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": "http://album-artist.attributed",
|
||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||
"image": {
|
||||
"type": "Link",
|
||||
"href": "http://cover.test/album-artist",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"joinphrase": "",
|
||||
"id": "http://lol.fr",
|
||||
"published": published.isoformat(),
|
||||
"name": "John Smith",
|
||||
}
|
||||
],
|
||||
"image": {
|
||||
|
@ -640,22 +761,28 @@ def test_federation_audio_track_to_metadata(now, mocker):
|
|||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"published": published.isoformat(),
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"content": "artist desc",
|
||||
"mediaType": "text/html",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": "http://artist.attributed",
|
||||
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
||||
"image": {
|
||||
"type": "Link",
|
||||
"href": "http://cover.test/artist",
|
||||
"mediaType": "image/png",
|
||||
"artist": {
|
||||
"published": published.isoformat(),
|
||||
"type": "Artist",
|
||||
"id": "http://hello.trackartist",
|
||||
"name": "Bob Smith",
|
||||
"content": "artist desc",
|
||||
"mediaType": "text/html",
|
||||
"musicbrainzId": str(uuid.uuid4()),
|
||||
"attributedTo": "http://artist.attributed",
|
||||
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
||||
"image": {
|
||||
"type": "Link",
|
||||
"href": "http://cover.test/artist",
|
||||
"mediaType": "image/png",
|
||||
},
|
||||
},
|
||||
"joinphrase": "",
|
||||
"id": "http://loli.fr",
|
||||
"published": published.isoformat(),
|
||||
"name": "Bob Smith",
|
||||
}
|
||||
],
|
||||
}
|
||||
|
@ -690,51 +817,61 @@ def test_federation_audio_track_to_metadata(now, mocker):
|
|||
"mimetype": serializer.validated_data["album"]["image"]["mediaType"],
|
||||
"url": serializer.validated_data["album"]["image"]["href"],
|
||||
},
|
||||
"artists": [
|
||||
"artist_credit": [
|
||||
{
|
||||
"name": a["name"],
|
||||
"mbid": a["musicbrainzId"],
|
||||
"fid": a["id"],
|
||||
"attributed_to": references["http://album-artist.attributed"],
|
||||
"fdate": serializer.validated_data["album"]["artists"][i][
|
||||
"artist": {
|
||||
"name": a["artist"]["name"],
|
||||
"mbid": a["artist"]["musicbrainzId"],
|
||||
"fid": a["artist"]["id"],
|
||||
"attributed_to": references["http://album-artist.attributed"],
|
||||
"fdate": serializer.validated_data["album"]["artist_credit"][i][
|
||||
"artist"
|
||||
]["published"],
|
||||
"description": {
|
||||
"content_type": "text/markdown",
|
||||
"text": "album artist desc",
|
||||
},
|
||||
"tags": ["AlbumArtistTag"],
|
||||
"cover_data": {
|
||||
"mimetype": serializer.validated_data["album"][
|
||||
"artist_credit"
|
||||
][i]["artist"]["image"]["mediaType"],
|
||||
"url": serializer.validated_data["album"]["artist_credit"][
|
||||
i
|
||||
]["artist"]["image"]["href"],
|
||||
},
|
||||
},
|
||||
"joinphrase": a["joinphrase"],
|
||||
"credit": a["artist"]["name"],
|
||||
}
|
||||
for i, a in enumerate(payload["album"]["artist_credit"])
|
||||
],
|
||||
},
|
||||
"artist_credit": [
|
||||
{
|
||||
"artist": {
|
||||
"name": a["artist"]["name"],
|
||||
"mbid": a["artist"]["musicbrainzId"],
|
||||
"fid": a["artist"]["id"],
|
||||
"fdate": serializer.validated_data["artist_credit"][i]["artist"][
|
||||
"published"
|
||||
],
|
||||
"description": {
|
||||
"content_type": "text/markdown",
|
||||
"text": "album artist desc",
|
||||
},
|
||||
"tags": ["AlbumArtistTag"],
|
||||
"attributed_to": references["http://artist.attributed"],
|
||||
"tags": ["ArtistTag"],
|
||||
"description": {"content_type": "text/html", "text": "artist desc"},
|
||||
"cover_data": {
|
||||
"mimetype": serializer.validated_data["album"]["artists"][i][
|
||||
"image"
|
||||
]["mediaType"],
|
||||
"url": serializer.validated_data["album"]["artists"][i][
|
||||
"mimetype": serializer.validated_data["artist_credit"][i][
|
||||
"artist"
|
||||
]["image"]["mediaType"],
|
||||
"url": serializer.validated_data["artist_credit"][i]["artist"][
|
||||
"image"
|
||||
]["href"],
|
||||
},
|
||||
}
|
||||
for i, a in enumerate(payload["album"]["artists"])
|
||||
],
|
||||
},
|
||||
# musicbrainz
|
||||
# federation
|
||||
"artists": [
|
||||
{
|
||||
"name": a["name"],
|
||||
"mbid": a["musicbrainzId"],
|
||||
"fid": a["id"],
|
||||
"fdate": serializer.validated_data["artists"][i]["published"],
|
||||
"attributed_to": references["http://artist.attributed"],
|
||||
"tags": ["ArtistTag"],
|
||||
"description": {"content_type": "text/html", "text": "artist desc"},
|
||||
"cover_data": {
|
||||
"mimetype": serializer.validated_data["artists"][i]["image"][
|
||||
"mediaType"
|
||||
],
|
||||
"url": serializer.validated_data["artists"][i]["image"]["href"],
|
||||
},
|
||||
"joinphrase": "",
|
||||
"credit": a["artist"]["name"],
|
||||
}
|
||||
for i, a in enumerate(payload["artists"])
|
||||
for i, a in enumerate(payload["artist_credit"])
|
||||
],
|
||||
}
|
||||
|
||||
|
@ -918,8 +1055,8 @@ def test_get_prunable_artists(factories):
|
|||
# non prunable artist
|
||||
non_prunable_artist = factories["music.Artist"]()
|
||||
non_prunable_album_artist = factories["music.Artist"]()
|
||||
factories["music.Track"](artist=non_prunable_artist)
|
||||
factories["music.Track"](album__artist=non_prunable_album_artist)
|
||||
factories["music.Track"](artist_credit__artist=non_prunable_artist)
|
||||
factories["music.Track"](album__artist_credit__artist=non_prunable_album_artist)
|
||||
|
||||
assert list(tasks.get_prunable_artists()) == [prunable_artist]
|
||||
|
||||
|
@ -997,7 +1134,7 @@ def test_get_track_from_import_metadata_with_forced_values(factories, mocker, fa
|
|||
assert track.disc_number == metadata["disc_number"]
|
||||
assert track.copyright == forced_values["copyright"]
|
||||
assert track.album == forced_values["album"]
|
||||
assert track.artist == forced_values["artist"]
|
||||
assert track.artist_credit.all()[0].artist == forced_values["artist"]
|
||||
assert track.attributed_to == forced_values["attributed_to"]
|
||||
assert track.license == forced_values["license"]
|
||||
assert (
|
||||
|
@ -1010,7 +1147,9 @@ def test_get_track_from_import_metadata_with_forced_values_album(
|
|||
factories, mocker, faker
|
||||
):
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](artist=channel.artist, with_cover=True)
|
||||
album = factories["music.Album"](
|
||||
artist_credit__artist=channel.artist, with_cover=True
|
||||
)
|
||||
|
||||
forced_values = {
|
||||
"title": "Real title",
|
||||
|
@ -1019,13 +1158,14 @@ def test_get_track_from_import_metadata_with_forced_values_album(
|
|||
upload = factories["music.Upload"](
|
||||
import_metadata=forced_values, library=channel.library, track=None
|
||||
)
|
||||
|
||||
tasks.process_upload(upload_id=upload.pk)
|
||||
upload.refresh_from_db()
|
||||
assert upload.import_status == "finished"
|
||||
|
||||
assert upload.track.title == forced_values["title"]
|
||||
assert upload.track.album == album
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
|
||||
|
||||
def test_process_channel_upload_forces_artist_and_attributed_to(
|
||||
|
@ -1073,7 +1213,7 @@ def test_process_channel_upload_forces_artist_and_attributed_to(
|
|||
assert upload.track.position == import_metadata["position"]
|
||||
assert upload.track.copyright == import_metadata["copyright"]
|
||||
assert upload.track.get_tags() == import_metadata["tags"]
|
||||
assert upload.track.artist == channel.artist
|
||||
assert upload.track.artist_credit.all()[0].artist == channel.artist
|
||||
assert upload.track.attributed_to == channel.attributed_to
|
||||
assert upload.track.attachment_cover == attachment
|
||||
|
||||
|
@ -1196,8 +1336,11 @@ def test_can_download_image_file_for_album_mbid(binary_cover, mocker, factories)
|
|||
|
||||
def test_can_import_track_with_same_mbid_in_different_albums(factories, mocker):
|
||||
artist = factories["music.Artist"]()
|
||||
artist_credit = factories["music.ArtistCredit"](artist=artist)
|
||||
upload = factories["music.Upload"](
|
||||
playable=True, track__artist=artist, track__album__artist=artist
|
||||
playable=True,
|
||||
track__artist_credit=artist_credit,
|
||||
track__album__artist_credit=artist_credit,
|
||||
)
|
||||
assert upload.track.mbid is not None
|
||||
data = {
|
||||
|
@ -1214,6 +1357,33 @@ def test_can_import_track_with_same_mbid_in_different_albums(factories, mocker):
|
|||
"mbid": upload.track.mbid,
|
||||
}
|
||||
|
||||
mb_ac = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": artist.mbid,
|
||||
"name": artist.name,
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": artist.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
mb_ac_album = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": artist.mbid,
|
||||
"name": artist.name,
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": artist.name,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
mocker.patch.object(tasks.musicbrainz.api.recordings, "get", return_value=mb_ac)
|
||||
mocker.patch.object(tasks.musicbrainz.api.releases, "get", return_value=mb_ac_album)
|
||||
mocker.patch.object(metadata.TrackMetadataSerializer, "validated_data", data)
|
||||
mocker.patch.object(tasks, "populate_album_cover")
|
||||
|
||||
|
@ -1228,8 +1398,12 @@ def test_can_import_track_with_same_mbid_in_different_albums(factories, mocker):
|
|||
|
||||
def test_import_track_with_same_mbid_in_same_albums_skipped(factories, mocker):
|
||||
artist = factories["music.Artist"]()
|
||||
artist_credit = factories["music.ArtistCredit"](artist=artist)
|
||||
|
||||
upload = factories["music.Upload"](
|
||||
playable=True, track__artist=artist, track__album__artist=artist
|
||||
playable=True,
|
||||
track__artist_credit=artist_credit,
|
||||
track__album__artist_credit=artist_credit,
|
||||
)
|
||||
assert upload.track.mbid is not None
|
||||
data = {
|
||||
|
@ -1261,8 +1435,8 @@ def test_can_import_track_with_same_position_in_different_discs(factories, mocke
|
|||
upload = factories["music.Upload"](playable=True)
|
||||
artist_data = [
|
||||
{
|
||||
"name": upload.track.album.artist.name,
|
||||
"mbid": upload.track.album.artist.mbid,
|
||||
"name": upload.track.album.artist_credit.all()[0].artist.name,
|
||||
"mbid": upload.track.album.artist_credit.all()[0].artist.mbid,
|
||||
}
|
||||
]
|
||||
data = {
|
||||
|
@ -1292,13 +1466,17 @@ def test_can_import_track_with_same_position_in_different_discs(factories, mocke
|
|||
|
||||
|
||||
def test_can_import_track_with_same_position_in_same_discs_skipped(factories, mocker):
|
||||
upload = factories["music.Upload"](playable=True)
|
||||
ac = factories["music.ArtistCredit"](joinphrase="", index=0)
|
||||
upload = factories["music.Upload"](
|
||||
playable=True, track__artist_credit=ac, track__album__artist_credit=ac
|
||||
)
|
||||
artist_data = [
|
||||
{
|
||||
"name": upload.track.album.artist.name,
|
||||
"mbid": upload.track.album.artist.mbid,
|
||||
"name": upload.track.album.artist_credit.all()[0].artist.name,
|
||||
"mbid": upload.track.album.artist_credit.all()[0].artist.mbid,
|
||||
}
|
||||
]
|
||||
|
||||
data = {
|
||||
"title": upload.track.title,
|
||||
"artists": artist_data,
|
||||
|
@ -1325,8 +1503,50 @@ def test_can_import_track_with_same_position_in_same_discs_skipped(factories, mo
|
|||
assert new_upload.import_status == "skipped"
|
||||
|
||||
|
||||
def test_update_track_metadata(factories):
|
||||
def test_update_track_metadata_no_mbid(factories):
|
||||
track = factories["music.Track"]()
|
||||
data = {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||
"artist": "Edvard Grieg",
|
||||
"album_artist": "Edvard Grieg; Musopen Symphony Orchestra",
|
||||
"album": "Peer Gynt Suite no. 1, op. 46",
|
||||
"date": "2012-08-15",
|
||||
"position": "4",
|
||||
"disc_number": "2",
|
||||
"license": "Dummy license: http://creativecommons.org/licenses/by-sa/4.0/",
|
||||
"copyright": "Someone",
|
||||
"comment": "hello there",
|
||||
"genre": "classical",
|
||||
}
|
||||
|
||||
tasks.update_track_metadata(metadata.FakeMetadata(data), track)
|
||||
|
||||
track.refresh_from_db()
|
||||
|
||||
assert track.title == data["title"]
|
||||
assert track.position == int(data["position"])
|
||||
assert track.disc_number == int(data["disc_number"])
|
||||
assert track.license.code == "cc-by-sa-4.0"
|
||||
assert track.copyright == data["copyright"]
|
||||
assert track.album.title == data["album"]
|
||||
assert track.album.release_date == datetime.date(2012, 8, 15)
|
||||
assert track.get_artist_credit_string == data["artist"]
|
||||
assert track.artist_credit.all()[0].artist.mbid is None
|
||||
assert (
|
||||
track.album.get_artist_credit_string
|
||||
== "Edvard Grieg; Musopen Symphony Orchestra"
|
||||
)
|
||||
assert track.album.artist_credit.all()[0].artist.mbid is None
|
||||
assert sorted(track.tagged_items.values_list("tag__name", flat=True)) == [
|
||||
"classical"
|
||||
]
|
||||
|
||||
|
||||
def test_update_track_metadata_mbid(factories, mocker):
|
||||
track = factories["music.Track"]()
|
||||
factories["music.Artist"](
|
||||
name="Edvard Grieg", mbid="013c8e5b-d72a-4cd3-8dee-6c64d6125823"
|
||||
)
|
||||
data = {
|
||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||
"artist": "Edvard Grieg",
|
||||
|
@ -1344,6 +1564,40 @@ def test_update_track_metadata(factories):
|
|||
"comment": "hello there",
|
||||
"genre": "classical",
|
||||
}
|
||||
mb_ac = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "013c8e5b-d72a-4cd3-8dee-6c64d6125823",
|
||||
"name": "Edvard Grieg",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Edvard Grieg",
|
||||
}
|
||||
]
|
||||
}
|
||||
mb_ac_album = {
|
||||
"artist-credit": [
|
||||
{
|
||||
"artist": {
|
||||
"id": "013c8e5b-d72a-4cd3-8dee-6c64d6125823",
|
||||
"name": "Edvard Grieg",
|
||||
},
|
||||
"joinphrase": "; ",
|
||||
"name": "Edvard Grieg",
|
||||
},
|
||||
{
|
||||
"artist": {
|
||||
"id": "5b4d7d2d-36df-4b38-95e3-a964234f520f",
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
},
|
||||
"joinphrase": "",
|
||||
"name": "Musopen Symphony Orchestra",
|
||||
},
|
||||
]
|
||||
}
|
||||
mocker.patch.object(tasks.musicbrainz.api.releases, "get", return_value=mb_ac_album)
|
||||
mocker.patch.object(tasks.musicbrainz.api.recordings, "get", return_value=mb_ac)
|
||||
tasks.update_track_metadata(metadata.FakeMetadata(data), track)
|
||||
|
||||
track.refresh_from_db()
|
||||
|
@ -1357,10 +1611,19 @@ def test_update_track_metadata(factories):
|
|||
assert track.album.title == data["album"]
|
||||
assert track.album.release_date == datetime.date(2012, 8, 15)
|
||||
assert str(track.album.mbid) == data["musicbrainz_albumid"]
|
||||
assert track.artist.name == data["artist"]
|
||||
assert str(track.artist.mbid) == data["musicbrainz_artistid"]
|
||||
assert track.album.artist.name == "Edvard Grieg"
|
||||
assert str(track.album.artist.mbid) == "013c8e5b-d72a-4cd3-8dee-6c64d6125823"
|
||||
assert track.get_artist_credit_string == data["artist"]
|
||||
assert (
|
||||
str(track.artist_credit.all()[0].artist.mbid)
|
||||
== "013c8e5b-d72a-4cd3-8dee-6c64d6125823"
|
||||
)
|
||||
assert (
|
||||
track.album.get_artist_credit_string
|
||||
== "Edvard Grieg; Musopen Symphony Orchestra"
|
||||
)
|
||||
assert (
|
||||
str(track.album.artist_credit.all()[0].artist.mbid)
|
||||
== "013c8e5b-d72a-4cd3-8dee-6c64d6125823"
|
||||
)
|
||||
assert sorted(track.tagged_items.values_list("tag__name", flat=True)) == [
|
||||
"classical"
|
||||
]
|
||||
|
@ -1450,3 +1713,189 @@ def test_upload_checks_mbid_tag_pass(temp_signal, factories, mocker, preferences
|
|||
upload.refresh_from_db()
|
||||
|
||||
assert upload.import_status == "finished"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw_string, expected",
|
||||
[
|
||||
(
|
||||
"The Kinks|Various Artists",
|
||||
[("The Kinks", "|", 0, None), ("Various Artists", "", 1, None)],
|
||||
),
|
||||
(
|
||||
"The Kinks,Various Artists",
|
||||
[("The Kinks", ",", 0, None), ("Various Artists", "", 1, None)],
|
||||
),
|
||||
(
|
||||
"Luigi 21 Plus feat. Ñejo feat Ñengo Flow & Chyno Nyno with Linkin Park and Evanescance",
|
||||
[
|
||||
("Luigi 21 Plus", " feat. ", 0, None),
|
||||
("Ñejo", " feat ", 1, None),
|
||||
("Ñengo Flow", " & ", 2, None),
|
||||
("Chyno Nyno", " with ", 3, None),
|
||||
("Linkin Park", " and ", 4, None),
|
||||
("Evanescance", "", 5, None),
|
||||
],
|
||||
),
|
||||
(
|
||||
"Bad Bunny x Poeta Callejero ; Mark B (Carlos Serrano & Carlos Martin Mambo Remix)",
|
||||
[
|
||||
("Bad Bunny", " x ", 0, None),
|
||||
("Poeta Callejero", " ; ", 1, None),
|
||||
("Mark B", " (", 2, None),
|
||||
("Carlos Serrano", " & ", 3, None),
|
||||
("Carlos Martin Mambo", " Remix)", 4, None),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_can_parse_multiples_artist(raw_string, expected):
|
||||
artist_credit = tasks.parse_credits(raw_string, None, None)
|
||||
assert artist_credit == expected
|
||||
|
||||
|
||||
# to do : what if an artist create a remix of a track ? artist_credit and title will match...
|
||||
def test_get_best_candidate_or_create_find_artist_credit(factories):
|
||||
track = factories["music.Track"]()
|
||||
query = Q(
|
||||
title__iexact=track.title,
|
||||
artist_credit__in=track.artist_credit.all(),
|
||||
position=track.position,
|
||||
disc_number=track.disc_number,
|
||||
)
|
||||
defaults = "lol"
|
||||
tasks.get_best_candidate_or_create(
|
||||
models.Track, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||
)
|
||||
|
||||
|
||||
def test_get_or_create_artists_credits_from_musicbrainz(factories, mocker):
|
||||
release_mb_response = {
|
||||
"status": "Official",
|
||||
"status-id": "4e304316-386d-3409-af2e-78857eec5cfe",
|
||||
"country": "XW",
|
||||
"text-representation": {"script": "Latn", "language": "spa"},
|
||||
"release-events": [
|
||||
{
|
||||
"date": "2019-05-30",
|
||||
"area": {
|
||||
"sort-name": "[Worldwide]",
|
||||
"id": "525d4e18-3d00-31b9-a58b-a146a916de8f",
|
||||
"disambiguation": "",
|
||||
"iso-3166-1-codes": ["XW"],
|
||||
"name": "[Worldwide]",
|
||||
},
|
||||
}
|
||||
],
|
||||
"disambiguation": "",
|
||||
"cover-art-archive": {
|
||||
"front": True,
|
||||
"count": 1,
|
||||
"back": False,
|
||||
"darkened": False,
|
||||
"artwork": True,
|
||||
},
|
||||
"id": "48cc978e-17b8-46ab-91e8-3dceef2725b5",
|
||||
"packaging-id": "119eba76-b343-3e02-a292-f0f00644bb9b",
|
||||
"packaging": "None",
|
||||
"date": "2019-05-30",
|
||||
"title": "#TBT",
|
||||
"artist-credit": [
|
||||
{
|
||||
"joinphrase": "",
|
||||
"artist": {
|
||||
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df",
|
||||
"disambiguation": 'Hiram David Santos Rojas, reggaeton artist aka "Lui-G 21+"',
|
||||
"name": "Luigi 21 Plus",
|
||||
"type": "Person",
|
||||
"sort-name": "Luigi 21 Plus",
|
||||
"id": "f1642d37-bbe2-4aff-a75e-86845ff49fa4",
|
||||
},
|
||||
"name": "Luigi 21 Plus",
|
||||
}
|
||||
],
|
||||
"quality": "normal",
|
||||
}
|
||||
recording_mb_response = {
|
||||
"length": 337000,
|
||||
"first-release-date": "2019-05-30",
|
||||
"disambiguation": "",
|
||||
"id": "cf3dacb7-3cee-430f-b0bb-cc4557158a03",
|
||||
"title": "Mueve ese culo puñeta",
|
||||
"artist-credit": [
|
||||
{
|
||||
"joinphrase": " feat. ",
|
||||
"artist": {
|
||||
"type": "Person",
|
||||
"id": "f1642d37-bbe2-4aff-a75e-86845ff49fa4",
|
||||
"sort-name": "Luigi 21 Plus",
|
||||
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df",
|
||||
"name": "Luigi 21 Plus",
|
||||
"disambiguation": 'Hiram David Santos Rojas, reggaeton artist aka "Lui-G 21+"',
|
||||
},
|
||||
"name": "Luigi 21 Plus",
|
||||
},
|
||||
{
|
||||
"name": "Ñejo",
|
||||
"artist": {
|
||||
"type": "Person",
|
||||
"id": "8248c905-689d-4e36-9def-7c515c5ef5eb",
|
||||
"name": "Ñejo",
|
||||
"disambiguation": "",
|
||||
"sort-name": "Ñejo",
|
||||
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df",
|
||||
},
|
||||
"joinphrase": ", ",
|
||||
},
|
||||
{
|
||||
"joinphrase": " & ",
|
||||
"artist": {
|
||||
"type": "Person",
|
||||
"id": "b7f5054e-c9de-49d8-b0eb-6deefb89b86b",
|
||||
"name": "Ñengo Flow",
|
||||
"disambiguation": "",
|
||||
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df",
|
||||
"sort-name": "Ñengo Flow",
|
||||
},
|
||||
"name": "Ñengo Flow",
|
||||
},
|
||||
{
|
||||
"joinphrase": "",
|
||||
"artist": {
|
||||
"id": "3d50191b-820c-4f6f-b25a-bc12d63e6718",
|
||||
"type": "Person",
|
||||
"type-id": "b6e035f4-3ce9-331c-97df-83397230b0df",
|
||||
"sort-name": "Chyno Nyno",
|
||||
"disambiguation": "",
|
||||
"name": "Chyno Nyno",
|
||||
},
|
||||
"name": "Chyno Nyno",
|
||||
},
|
||||
],
|
||||
"video": False,
|
||||
}
|
||||
for mb_type, mb_response in [
|
||||
("release", release_mb_response),
|
||||
("recording", recording_mb_response),
|
||||
]:
|
||||
mocker.patch.object(
|
||||
tasks.musicbrainz.api.releases, "get", return_value=mb_response
|
||||
)
|
||||
mocker.patch.object(
|
||||
tasks.musicbrainz.api.recordings, "get", return_value=mb_response
|
||||
)
|
||||
tasks.get_or_create_artists_credits_from_musicbrainz(
|
||||
mb_type, mb_response["id"], None, None
|
||||
)
|
||||
for i, ac in enumerate(mb_response["artist-credit"]):
|
||||
ac = models.ArtistCredit.objects.get(
|
||||
artist__name=ac["artist"]["name"],
|
||||
joinphrase=ac["joinphrase"],
|
||||
credit=ac["name"],
|
||||
)
|
||||
|
||||
assert ac.artist.name == mb_response["artist-credit"][i]["artist"]["name"]
|
||||
assert (
|
||||
str(ac.artist.mbid) == mb_response["artist-credit"][i]["artist"]["id"]
|
||||
)
|
||||
assert ac.joinphrase == mb_response["artist-credit"][i]["joinphrase"]
|
||||
|
|
|
@ -26,12 +26,12 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
|
|||
track = factories["music.Upload"](
|
||||
library__privacy_level="everyone",
|
||||
import_status="finished",
|
||||
track__album__artist__set_tags=tags,
|
||||
track__album__artist_credit__artist__set_tags=tags,
|
||||
).track
|
||||
artist = track.artist
|
||||
artist = track.artist_credit.all()[0].artist
|
||||
request = api_request.get("/")
|
||||
qs = artist.__class__.objects.with_albums().prefetch_related(
|
||||
Prefetch("tracks", to_attr="_prefetched_tracks")
|
||||
Prefetch("artist_credit__tracks", to_attr="_prefetched_tracks")
|
||||
)
|
||||
serializer = serializers.ArtistWithAlbumsSerializer(
|
||||
qs, many=True, context={"request": request}
|
||||
|
@ -118,7 +118,9 @@ def test_artist_view_filter_playable(param, expected, factories, api_request):
|
|||
"empty": factories["music.Artist"](),
|
||||
"full": factories["music.Upload"](
|
||||
library__privacy_level="everyone", import_status="finished"
|
||||
).track.artist,
|
||||
)
|
||||
.track.artist_credit.all()[0]
|
||||
.artist,
|
||||
}
|
||||
|
||||
request = api_request.get("/", {"playable": param})
|
||||
|
@ -817,7 +819,7 @@ def test_user_can_create_upload_in_channel(
|
|||
channel = factories["audio.Channel"](attributed_to=actor)
|
||||
url = reverse("api:v1:uploads-list")
|
||||
m = mocker.patch("funkwhale_api.common.utils.on_commit")
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
response = logged_in_api_client.post(
|
||||
url,
|
||||
{
|
||||
|
@ -968,12 +970,14 @@ def test_can_get_libraries_for_music_entities(
|
|||
library = upload.library
|
||||
setattr(library, "_uploads_count", 1)
|
||||
data = {
|
||||
"artist": upload.track.artist,
|
||||
"artist": upload.track.artist_credit.all()[0].artist,
|
||||
"album": upload.track.album,
|
||||
"track": upload.track,
|
||||
}
|
||||
# libraries in channel should be missing excluded
|
||||
channel = factories["audio.Channel"](artist=upload.track.artist)
|
||||
channel = factories["audio.Channel"](
|
||||
artist=upload.track.artist_credit.all()[0].artist
|
||||
)
|
||||
factories["music.Upload"](
|
||||
library=channel.library, playable=True, track=upload.track
|
||||
)
|
||||
|
@ -1035,7 +1039,7 @@ def test_oembed_track(factories, no_api_auth, api_client, settings):
|
|||
"provider_url": settings.FUNKWHALE_URL,
|
||||
"height": 150,
|
||||
"width": 600,
|
||||
"title": f"{track.title} by {track.artist.name}",
|
||||
"title": f"{track.title} by {track.artist_credit.all()[0].artist.name}",
|
||||
"description": track.full_name,
|
||||
"thumbnail_url": federation_utils.full_url(
|
||||
track.album.attachment_cover.file.crop["200x200"].url
|
||||
|
@ -1045,9 +1049,11 @@ def test_oembed_track(factories, no_api_auth, api_client, settings):
|
|||
"html": '<iframe width="600" height="150" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
|
||||
iframe_src
|
||||
),
|
||||
"author_name": track.artist.name,
|
||||
"author_name": track.artist_credit.all()[0].artist.name,
|
||||
"author_url": federation_utils.full_url(
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": track.artist.pk})
|
||||
utils.spa_reverse(
|
||||
"library_artist", kwargs={"pk": track.artist_credit.all()[0].artist.pk}
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -1071,8 +1077,8 @@ def test_oembed_album(factories, no_api_auth, api_client, settings):
|
|||
"provider_url": settings.FUNKWHALE_URL,
|
||||
"height": 400,
|
||||
"width": 600,
|
||||
"title": f"{album.title} by {album.artist.name}",
|
||||
"description": f"{album.title} by {album.artist.name}",
|
||||
"title": f"{album.title} by {album.artist_credit.all()[0].artist.name}",
|
||||
"description": f"{album.title} by {album.artist_credit.all()[0].artist.name}",
|
||||
"thumbnail_url": federation_utils.full_url(
|
||||
album.attachment_cover.file.crop["200x200"].url
|
||||
),
|
||||
|
@ -1081,9 +1087,11 @@ def test_oembed_album(factories, no_api_auth, api_client, settings):
|
|||
"html": '<iframe width="600" height="400" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
|
||||
iframe_src
|
||||
),
|
||||
"author_name": album.artist.name,
|
||||
"author_name": album.artist_credit.all()[0].artist.name,
|
||||
"author_url": federation_utils.full_url(
|
||||
utils.spa_reverse("library_artist", kwargs={"pk": album.artist.pk})
|
||||
utils.spa_reverse(
|
||||
"library_artist", kwargs={"pk": album.artist_credit.all()[0].artist.pk}
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
|
@ -1097,7 +1105,7 @@ def test_oembed_artist(factories, no_api_auth, api_client, settings):
|
|||
settings.FUNKWHALE_EMBED_URL = "http://embed"
|
||||
track = factories["music.Track"](album__with_cover=True)
|
||||
album = track.album
|
||||
artist = track.artist
|
||||
artist = track.artist_credit.all()[0].artist
|
||||
url = reverse("api:v1:oembed")
|
||||
artist_url = f"https://test.com/library/artists/{artist.pk}"
|
||||
iframe_src = f"http://embed?type=artist&id={artist.pk}"
|
||||
|
@ -1281,7 +1289,7 @@ def test_artist_list_exclude_channels(
|
|||
)
|
||||
def test_album_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||
channel_artist = factories["audio.Channel"]().artist
|
||||
factories["music.Album"](artist=channel_artist)
|
||||
factories["music.Album"](artist_credit__artist=channel_artist)
|
||||
|
||||
url = reverse("api:v1:albums-list")
|
||||
response = logged_in_api_client.get(url, params)
|
||||
|
@ -1296,7 +1304,7 @@ def test_album_list_exclude_channels(params, expected, factories, logged_in_api_
|
|||
)
|
||||
def test_track_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||
channel_artist = factories["audio.Channel"]().artist
|
||||
factories["music.Track"](artist=channel_artist)
|
||||
factories["music.Track"](artist_credit__artist=channel_artist)
|
||||
|
||||
url = reverse("api:v1:tracks-list")
|
||||
response = logged_in_api_client.get(url, params)
|
||||
|
@ -1404,10 +1412,12 @@ def test_channel_owner_can_create_album(factories, logged_in_api_client):
|
|||
actor = logged_in_api_client.user.create_actor()
|
||||
channel = factories["audio.Channel"](attributed_to=actor, artist__with_cover=True)
|
||||
attachment = factories["common.Attachment"](actor=actor)
|
||||
ac = factories["music.ArtistCredit"](artist=channel.artist)
|
||||
|
||||
url = reverse("api:v1:albums-list")
|
||||
|
||||
data = {
|
||||
"artist": channel.artist.pk,
|
||||
"artist_credit": serializers.ArtistCreditSerializer(ac).data,
|
||||
"cover": attachment.uuid,
|
||||
"title": "Hello world",
|
||||
"release_date": "2019-01-02",
|
||||
|
@ -1437,7 +1447,7 @@ def test_channel_owner_can_delete_album(factories, logged_in_api_client, mocker)
|
|||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
channel = factories["audio.Channel"](attributed_to=actor)
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
url = reverse("api:v1:albums-detail", kwargs={"pk": album.pk})
|
||||
|
||||
response = logged_in_api_client.delete(url)
|
||||
|
@ -1474,7 +1484,7 @@ def test_other_user_cannot_create_album(factories, logged_in_api_client):
|
|||
def test_other_user_cannot_delete_album(factories, logged_in_api_client):
|
||||
logged_in_api_client.user.create_actor()
|
||||
channel = factories["audio.Channel"]()
|
||||
album = factories["music.Album"](artist=channel.artist)
|
||||
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||
url = reverse("api:v1:albums-detail", kwargs={"pk": album.pk})
|
||||
|
||||
response = logged_in_api_client.delete(url)
|
||||
|
@ -1487,7 +1497,7 @@ def test_channel_owner_can_delete_track(factories, logged_in_api_client, mocker)
|
|||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
channel = factories["audio.Channel"](attributed_to=actor)
|
||||
track = factories["music.Track"](artist=channel.artist)
|
||||
track = factories["music.Track"](artist_credit__artist=channel.artist)
|
||||
upload1 = factories["music.Upload"](track=track)
|
||||
upload2 = factories["music.Upload"](track=track)
|
||||
url = reverse("api:v1:tracks-detail", kwargs={"pk": track.pk})
|
||||
|
@ -1506,7 +1516,7 @@ def test_channel_owner_can_delete_track(factories, logged_in_api_client, mocker)
|
|||
def test_other_user_cannot_delete_track(factories, logged_in_api_client):
|
||||
logged_in_api_client.user.create_actor()
|
||||
channel = factories["audio.Channel"]()
|
||||
track = factories["music.Track"](artist=channel.artist)
|
||||
track = factories["music.Track"](artist_credit__artist=channel.artist)
|
||||
url = reverse("api:v1:tracks-detail", kwargs={"pk": track.pk})
|
||||
|
||||
response = logged_in_api_client.delete(url)
|
||||
|
|
|
@ -23,6 +23,8 @@ def test_playlist_filter_artist(factories, queryset_equal_list):
|
|||
plt = factories["playlists.PlaylistTrack"]()
|
||||
factories["playlists.PlaylistTrack"]()
|
||||
qs = models.Playlist.objects.all()
|
||||
filterset = filters.PlaylistFilter({"artist": plt.track.artist.pk}, queryset=qs)
|
||||
filterset = filters.PlaylistFilter(
|
||||
{"artist": plt.track.artist_credit.all()[0].artist.pk}, queryset=qs
|
||||
)
|
||||
|
||||
assert filterset.qs == [plt.playlist]
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import pytest
|
||||
from itertools import chain
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.music.serializers import TrackSerializer
|
||||
|
@ -20,9 +22,11 @@ def test_can_list_config_options(logged_in_api_client):
|
|||
def test_can_validate_config(logged_in_api_client, factories):
|
||||
artist1 = factories["music.Artist"]()
|
||||
artist2 = factories["music.Artist"]()
|
||||
factories["music.Track"].create_batch(3, artist=artist1)
|
||||
factories["music.Track"].create_batch(3, artist=artist2)
|
||||
candidates = artist1.tracks.order_by("pk")
|
||||
factories["music.Track"].create_batch(3, artist_credit__artist=artist1)
|
||||
factories["music.Track"].create_batch(3, artist_credit__artist=artist2)
|
||||
candidates = list(
|
||||
chain(*[ac.tracks.order_by("pk") for ac in artist1.artist_credit.all()])
|
||||
)
|
||||
f = {"filters": [{"type": "artist", "ids": [artist1.pk]}]}
|
||||
url = reverse("api:v1:radios:radios-validate")
|
||||
response = logged_in_api_client.post(url, f, format="json")
|
||||
|
@ -32,7 +36,7 @@ def test_can_validate_config(logged_in_api_client, factories):
|
|||
payload = response.data
|
||||
|
||||
expected = {
|
||||
"count": candidates.count(),
|
||||
"count": len(candidates),
|
||||
"sample": TrackSerializer(candidates, many=True).data,
|
||||
}
|
||||
|
||||
|
|
|
@ -91,7 +91,9 @@ def test_can_get_choices_for_favorites_radio(factories):
|
|||
|
||||
def test_can_get_choices_for_custom_radio(factories):
|
||||
artist = factories["music.Artist"]()
|
||||
files = factories["music.Upload"].create_batch(5, track__artist=artist)
|
||||
files = factories["music.Upload"].create_batch(
|
||||
5, track__artist_credit__artist=artist
|
||||
)
|
||||
tracks = [f.track for f in files]
|
||||
factories["music.Upload"].create_batch(5)
|
||||
|
||||
|
@ -208,7 +210,9 @@ def test_can_start_artist_radio(factories):
|
|||
user = factories["users.User"]()
|
||||
artist = factories["music.Artist"]()
|
||||
factories["music.Upload"].create_batch(5)
|
||||
good_files = factories["music.Upload"].create_batch(5, track__artist=artist)
|
||||
good_files = factories["music.Upload"].create_batch(
|
||||
5, track__artist_credit__artist=artist
|
||||
)
|
||||
good_tracks = [f.track for f in good_files]
|
||||
|
||||
radio = radios.ArtistRadio()
|
||||
|
@ -224,7 +228,7 @@ def test_can_start_tag_radio(factories):
|
|||
good_tracks = [
|
||||
factories["music.Track"](set_tags=[tag.name]),
|
||||
factories["music.Track"](album__set_tags=[tag.name]),
|
||||
factories["music.Track"](album__artist__set_tags=[tag.name]),
|
||||
factories["music.Track"](album__artist_credit__artist__set_tags=[tag.name]),
|
||||
]
|
||||
factories["music.Track"].create_batch(3, set_tags=["notrock"])
|
||||
|
||||
|
@ -358,7 +362,7 @@ def test_session_radio_get_queryset_ignore_filtered_track_artist(
|
|||
factories, queryset_equal_list
|
||||
):
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
factories["music.Track"](artist=cf.target_artist)
|
||||
factories["music.Track"](artist_credit__artist=cf.target_artist)
|
||||
valid_track = factories["music.Track"]()
|
||||
radio = radios.RandomRadio()
|
||||
radio.start_session(user=cf.user)
|
||||
|
@ -370,7 +374,7 @@ def test_session_radio_get_queryset_ignore_filtered_track_album_artist(
|
|||
factories, queryset_equal_list
|
||||
):
|
||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||
factories["music.Track"](album__artist=cf.target_artist)
|
||||
factories["music.Track"](album__artist_credit__artist=cf.target_artist)
|
||||
valid_track = factories["music.Track"]()
|
||||
radio = radios.RandomRadio()
|
||||
radio.start_session(user=cf.user)
|
||||
|
@ -382,9 +386,11 @@ def test_get_choices_for_custom_radio_exclude_artist(factories):
|
|||
included_artist = factories["music.Artist"]()
|
||||
excluded_artist = factories["music.Artist"]()
|
||||
included_uploads = factories["music.Upload"].create_batch(
|
||||
5, track__artist=included_artist
|
||||
5, track__artist_credit__artist=included_artist
|
||||
)
|
||||
factories["music.Upload"].create_batch(
|
||||
5, track__artist_credit__artist=excluded_artist
|
||||
)
|
||||
factories["music.Upload"].create_batch(5, track__artist=excluded_artist)
|
||||
|
||||
session = factories["radios.CustomRadioSession"](
|
||||
custom_radio__config=[
|
||||
|
@ -422,7 +428,9 @@ def test_can_start_custom_multiple_radio_from_api(api_client, factories):
|
|||
map_filters_to_type = {"tags": "names", "artists": "ids", "playlists": "names"}
|
||||
for key, value in map_filters_to_type.items():
|
||||
attr = value[:-1]
|
||||
track_filter_key = [getattr(a.artist, attr) for a in tracks]
|
||||
track_filter_key = [
|
||||
getattr(a.artist_credit.all()[0].artist, attr) for a in tracks
|
||||
]
|
||||
config = {"filters": [{"type": key, value: track_filter_key}]}
|
||||
response = api_client.post(
|
||||
url,
|
||||
|
|
|
@ -95,7 +95,9 @@ def test_can_get_choices_for_favorites_radio_v2(factories):
|
|||
|
||||
def test_can_get_choices_for_custom_radio_v2(factories):
|
||||
artist = factories["music.Artist"]()
|
||||
files = factories["music.Upload"].create_batch(5, track__artist=artist)
|
||||
files = factories["music.Upload"].create_batch(
|
||||
5, track__artist_credit__artist=artist
|
||||
)
|
||||
tracks = [f.track for f in files]
|
||||
factories["music.Upload"].create_batch(5)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ def test_get_valid_filepart(input, expected):
|
|||
[
|
||||
(
|
||||
{
|
||||
"artist__name": "Hello",
|
||||
"artist_credit__artist__name": "Hello",
|
||||
"album__title": "World",
|
||||
"title": "foo",
|
||||
"position": None,
|
||||
|
@ -47,7 +47,7 @@ def test_get_valid_filepart(input, expected):
|
|||
),
|
||||
(
|
||||
{
|
||||
"artist__name": "AC/DC",
|
||||
"artist_credit__artist__name": "AC/DC",
|
||||
"album__title": "escape/my",
|
||||
"title": "sla/sh",
|
||||
"position": 12,
|
||||
|
@ -76,8 +76,8 @@ def test_get_artists_serializer(factories):
|
|||
name="", with_cover=False
|
||||
) # Shouldn't be serialised
|
||||
|
||||
factories["music.Album"].create_batch(size=3, artist=artist1)
|
||||
factories["music.Album"].create_batch(size=2, artist=artist2)
|
||||
factories["music.Album"].create_batch(size=3, artist_credit__artist=artist1)
|
||||
factories["music.Album"].create_batch(size=2, artist_credit__artist=artist2)
|
||||
|
||||
expected = {
|
||||
"ignoredArticles": "",
|
||||
|
@ -125,7 +125,7 @@ def test_get_artists_serializer(factories):
|
|||
|
||||
def test_get_artist_serializer(factories):
|
||||
artist = factories["music.Artist"](with_cover=True)
|
||||
album = factories["music.Album"](artist=artist, with_cover=True)
|
||||
album = factories["music.Album"](artist_credit__artist=artist, with_cover=True)
|
||||
tracks = factories["music.Track"].create_batch(size=3, album=album)
|
||||
|
||||
expected = {
|
||||
|
@ -191,7 +191,7 @@ def test_get_track_data_content_type(mimetype, extension, expected, factories):
|
|||
|
||||
def test_get_album_serializer(factories):
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist, with_cover=True)
|
||||
album = factories["music.Album"](artist_credit__artist=artist, with_cover=True)
|
||||
track = factories["music.Track"](album=album, disc_number=42)
|
||||
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
|
||||
tagged_item = factories["tags.TaggedItem"](content_object=album, tag__name="foo")
|
||||
|
@ -243,8 +243,8 @@ def test_get_album_serializer(factories):
|
|||
|
||||
|
||||
def test_starred_tracks2_serializer(factories):
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
track = factories["music.Track"](album=album)
|
||||
upload = factories["music.Upload"](track=track)
|
||||
favorite = factories["favorites.TrackFavorite"](track=track)
|
||||
|
@ -347,7 +347,7 @@ def test_channel_episode_serializer(factories):
|
|||
description = factories["common.Content"]()
|
||||
channel = factories["audio.Channel"]()
|
||||
track = factories["music.Track"](
|
||||
description=description, artist=channel.artist, with_cover=True
|
||||
description=description, artist_credit__artist=channel.artist, with_cover=True
|
||||
)
|
||||
upload = factories["music.Upload"](
|
||||
playable=True, track=track, bitrate=128000, duration=42
|
||||
|
|
|
@ -152,7 +152,9 @@ def test_get_artist(
|
|||
url = reverse("api:subsonic:subsonic-get_artist")
|
||||
assert url.endswith("getArtist") is True
|
||||
artist = factories["music.Artist"](playable=True)
|
||||
factories["music.Album"].create_batch(size=3, artist=artist, playable=True)
|
||||
factories["music.Album"].create_batch(
|
||||
size=3, artist_credit__artist=artist, playable=True
|
||||
)
|
||||
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
|
||||
|
||||
expected = {"artist": serializers.GetArtistSerializer(artist).data}
|
||||
|
@ -202,9 +204,9 @@ def test_get_album(
|
|||
):
|
||||
url = reverse("api:subsonic:subsonic-get_album")
|
||||
assert url.endswith("getAlbum") is True
|
||||
artist = factories["music.Artist"]()
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = (
|
||||
factories["music.Album"](artist=artist)
|
||||
factories["music.Album"](artist_credit=artist_credit)
|
||||
.__class__.objects.with_duration()
|
||||
.first()
|
||||
)
|
||||
|
@ -217,7 +219,10 @@ def test_get_album(
|
|||
assert response.data == expected
|
||||
|
||||
playable_by.assert_called_once_with(
|
||||
music_models.Album.objects.with_duration().select_related("artist"), None
|
||||
music_models.Album.objects.with_duration().prefetch_related(
|
||||
"artist_credit__artist"
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
|
||||
|
@ -227,8 +232,8 @@ def test_get_song(
|
|||
):
|
||||
url = reverse("api:subsonic:subsonic-get_song")
|
||||
assert url.endswith("getSong") is True
|
||||
artist = factories["music.Artist"]()
|
||||
album = factories["music.Album"](artist=artist)
|
||||
artist_credit = factories["music.ArtistCredit"]()
|
||||
album = factories["music.Album"](artist_credit=artist_credit)
|
||||
track = factories["music.Track"](album=album, playable=True)
|
||||
upload = factories["music.Upload"](track=track)
|
||||
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
|
||||
|
@ -508,10 +513,12 @@ def test_get_album_list2_by_genre(f, db, logged_in_api_client, factories):
|
|||
url = reverse("api:subsonic:subsonic-get_album_list2")
|
||||
assert url.endswith("getAlbumList2") is True
|
||||
album1 = factories["music.Album"](
|
||||
artist__name="Artist1", playable=True, set_tags=["Rock"]
|
||||
artist_credit__artist__name="Artist1", playable=True, set_tags=["Rock"]
|
||||
).__class__.objects.with_duration()[0]
|
||||
album2 = factories["music.Album"](
|
||||
artist__name="Artist2", playable=True, artist__set_tags=["Rock"]
|
||||
artist_credit__artist__name="Artist2",
|
||||
playable=True,
|
||||
artist_credit__artist__set_tags=["Rock"],
|
||||
).__class__.objects.with_duration()[1]
|
||||
factories["music.Album"](playable=True, set_tags=["Pop"])
|
||||
response = logged_in_api_client.get(
|
||||
|
@ -556,7 +563,12 @@ def test_get_album_list2_by_year(params, expected, db, logged_in_api_client, fac
|
|||
@pytest.mark.parametrize("f", ["json"])
|
||||
@pytest.mark.parametrize(
|
||||
"tags_field",
|
||||
["set_tags", "artist__set_tags", "album__set_tags", "album__artist__set_tags"],
|
||||
[
|
||||
"set_tags",
|
||||
"artist_credit__artist__set_tags",
|
||||
"album__set_tags",
|
||||
"album__artist_credit__artist__set_tags",
|
||||
],
|
||||
)
|
||||
def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
|
||||
url = reverse("api:subsonic:subsonic-get_songs_by_genre")
|
||||
|
@ -916,20 +928,22 @@ def test_get_podcasts(logged_in_api_client, factories, mocker):
|
|||
)
|
||||
upload1 = factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
library=channel.library,
|
||||
bitrate=128000,
|
||||
duration=42,
|
||||
)
|
||||
upload2 = factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
library=channel.library,
|
||||
bitrate=256000,
|
||||
duration=43,
|
||||
)
|
||||
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
|
||||
factories["music.Upload"](import_status="pending", track__artist=channel.artist)
|
||||
factories["music.Upload"](
|
||||
import_status="pending", track__artist_credit__artist=channel.artist
|
||||
)
|
||||
factories["audio.Channel"](external=True)
|
||||
factories["federation.Follow"]()
|
||||
url = reverse("api:subsonic:subsonic-get_podcasts")
|
||||
|
@ -953,14 +967,14 @@ def test_get_podcasts_by_id(logged_in_api_client, factories, mocker):
|
|||
)
|
||||
upload1 = factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel1.artist,
|
||||
track__artist_credit__artist=channel1.artist,
|
||||
library=channel1.library,
|
||||
bitrate=128000,
|
||||
duration=42,
|
||||
)
|
||||
factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel2.artist,
|
||||
track__artist_credit__artist=channel2.artist,
|
||||
library=channel2.library,
|
||||
bitrate=256000,
|
||||
duration=43,
|
||||
|
@ -983,14 +997,14 @@ def test_get_newest_podcasts(logged_in_api_client, factories, mocker):
|
|||
)
|
||||
upload1 = factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
library=channel.library,
|
||||
bitrate=128000,
|
||||
duration=42,
|
||||
)
|
||||
upload2 = factories["music.Upload"](
|
||||
playable=True,
|
||||
track__artist=channel.artist,
|
||||
track__artist_credit__artist=channel.artist,
|
||||
library=channel.library,
|
||||
bitrate=256000,
|
||||
duration=43,
|
||||
|
|
|
@ -6,9 +6,11 @@ def test_get_tags_from_foreign_key(factories):
|
|||
rock_tag = factories["tags.Tag"](name="Rock")
|
||||
rap_tag = factories["tags.Tag"](name="Rap")
|
||||
artist = factories["music.Artist"]()
|
||||
factories["music.Track"].create_batch(3, artist=artist, set_tags=["rock", "rap"])
|
||||
factories["music.Track"].create_batch(
|
||||
3, artist=artist, set_tags=["rock", "rap", "techno"]
|
||||
3, artist_credit__artist=artist, set_tags=["rock", "rap"]
|
||||
)
|
||||
factories["music.Track"].create_batch(
|
||||
3, artist_credit__artist=artist, set_tags=["rock", "rap", "techno"]
|
||||
)
|
||||
|
||||
result = tasks.get_tags_from_foreign_key(
|
||||
|
|
|
@ -11,13 +11,13 @@ def test_resolve_recordings_to_fw_track(mocker, factories):
|
|||
factories["music.Track"](
|
||||
pk=1,
|
||||
title="I Want It That Way",
|
||||
artist=artist,
|
||||
artist_credit__artist=artist,
|
||||
mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa",
|
||||
)
|
||||
factories["music.Track"](
|
||||
pk=2,
|
||||
title="I Want It That Way",
|
||||
artist=artist,
|
||||
artist_credit__artist=artist,
|
||||
)
|
||||
|
||||
client = typesense.Client(
|
||||
|
|
|
@ -58,7 +58,8 @@ const groups = computed(() => [
|
|||
settings: [
|
||||
{ name: 'music__transcoding_enabled' },
|
||||
{ name: 'music__transcoding_cache_duration' },
|
||||
{ name: 'music__only_allow_musicbrainz_tagged_files' }
|
||||
{ name: 'music__only_allow_musicbrainz_tagged_files' },
|
||||
{ name: 'music__join_phrases' }
|
||||
|
||||
]
|
||||
},
|
||||
|
|
Ładowanie…
Reference in New Issue