Supporting multi-artist per tracks/albums (#1568)
This commit is contained in:
parent
007fe3b192
commit
3b5de1a32d
api
funkwhale_api
activity
audio
common/management/commands
contrib
favorites
federation
history
instance
manage
moderation
music
admin.pydynamic_preferences_registry.pyfactories.pyfilters.pyimporters.py
management/commands
metadata.pymigrations
models.pymutations.pyserializers.pyspa_views.pytasks.pyutils.pyviews.pymusicbrainz
playlists
radios
subsonic
tags
typesense
tests
audio
cli
common
favorites
federation
history
manage
music
test_filters.pytest_metadata.pytest_migrations.pytest_models.pytest_music.pytest_serializers.pytest_spa_views.pytest_tasks.pytest_views.py
playlists
radios
subsonic
tags
typesense
changes/changelog.d
docs/specs/multi-artist
front/src
App.vue
components
|
@ -40,11 +40,23 @@ def combined_recent(limit, **kwargs):
|
||||||
def get_activity(user, limit=20):
|
def get_activity(user, limit=20):
|
||||||
query = fields.privacy_level_query(user, lookup_field="user__privacy_level")
|
query = fields.privacy_level_query(user, lookup_field="user__privacy_level")
|
||||||
querysets = [
|
querysets = [
|
||||||
Listening.objects.filter(query).select_related(
|
Listening.objects.filter(query)
|
||||||
"track", "user", "track__artist", "track__album__artist"
|
.select_related(
|
||||||
|
"track",
|
||||||
|
"user",
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"track__artist_credit__artist",
|
||||||
|
"track__album__artist_credit__artist",
|
||||||
),
|
),
|
||||||
TrackFavorite.objects.filter(query).select_related(
|
TrackFavorite.objects.filter(query)
|
||||||
"track", "user", "track__artist", "track__album__artist"
|
.select_related(
|
||||||
|
"track",
|
||||||
|
"user",
|
||||||
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"track__artist_credit__artist",
|
||||||
|
"track__album__artist_credit__artist",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
records = combined_recent(limit=limit, querysets=querysets)
|
records = combined_recent(limit=limit, querysets=querysets)
|
||||||
|
|
|
@ -21,7 +21,11 @@ TAG_FILTER = common_filters.MultipleQueryFilter(method=filter_tags)
|
||||||
|
|
||||||
class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
q = fields.SearchFilter(
|
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
|
tag = TAG_FILTER
|
||||||
scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
|
scope = common_filters.ActorScopeFilter(actor_field="attributed_to", distinct=True)
|
||||||
|
@ -33,7 +37,7 @@ class ChannelFilter(moderation_filters.HiddenContentFilterSet):
|
||||||
# tuple-mapping retains order
|
# tuple-mapping retains order
|
||||||
fields=(
|
fields=(
|
||||||
("creation_date", "creation_date"),
|
("creation_date", "creation_date"),
|
||||||
("artist__modification_date", "modification_date"),
|
("artist_credit__artist__modification_date", "modification_date"),
|
||||||
("?", "random"),
|
("?", "random"),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -26,6 +26,7 @@ from funkwhale_api.federation import serializers as federation_serializers
|
||||||
from funkwhale_api.federation import utils as federation_utils
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
from funkwhale_api.moderation import mrf
|
from funkwhale_api.moderation import mrf
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
|
from funkwhale_api.music import tasks
|
||||||
from funkwhale_api.music.serializers import COVER_WRITE_FIELD, CoverField
|
from funkwhale_api.music.serializers import COVER_WRITE_FIELD, CoverField
|
||||||
from funkwhale_api.tags import models as tags_models
|
from funkwhale_api.tags import models as tags_models
|
||||||
from funkwhale_api.tags import serializers as tags_serializers
|
from funkwhale_api.tags import serializers as tags_serializers
|
||||||
|
@ -246,11 +247,14 @@ class SimpleChannelArtistSerializer(serializers.Serializer):
|
||||||
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
||||||
cover = CoverField(allow_null=True, required=False)
|
cover = CoverField(allow_null=True, required=False)
|
||||||
channel = serializers.UUIDField(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.SerializerMethodField(required=False)
|
||||||
tags = serializers.ListField(
|
tags = serializers.ListField(
|
||||||
child=serializers.CharField(), source="_prefetched_tagged_items", required=False
|
child=serializers.CharField(), source="_prefetched_tagged_items", required=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_tracks_count(self, o) -> int:
|
||||||
|
return getattr(o, "_tracks_count", 0)
|
||||||
|
|
||||||
|
|
||||||
class ChannelSerializer(serializers.ModelSerializer):
|
class ChannelSerializer(serializers.ModelSerializer):
|
||||||
artist = SimpleChannelArtistSerializer()
|
artist = SimpleChannelArtistSerializer()
|
||||||
|
@ -749,7 +753,7 @@ class RssFeedItemSerializer(serializers.Serializer):
|
||||||
else:
|
else:
|
||||||
existing_track = (
|
existing_track = (
|
||||||
music_models.Track.objects.filter(
|
music_models.Track.objects.filter(
|
||||||
uuid=expected_uuid, artist__channel=channel
|
uuid=expected_uuid, artist_credit__artist__channel=channel
|
||||||
)
|
)
|
||||||
.select_related("description", "attachment_cover")
|
.select_related("description", "attachment_cover")
|
||||||
.first()
|
.first()
|
||||||
|
@ -765,7 +769,6 @@ class RssFeedItemSerializer(serializers.Serializer):
|
||||||
"disc_number": validated_data.get("itunes_season", 1) or 1,
|
"disc_number": validated_data.get("itunes_season", 1) or 1,
|
||||||
"position": validated_data.get("itunes_episode", 1) or 1,
|
"position": validated_data.get("itunes_episode", 1) or 1,
|
||||||
"title": validated_data["title"],
|
"title": validated_data["title"],
|
||||||
"artist": channel.artist,
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
if "rights" in validated_data:
|
if "rights" in validated_data:
|
||||||
|
@ -801,6 +804,21 @@ class RssFeedItemSerializer(serializers.Serializer):
|
||||||
**track_kwargs,
|
**track_kwargs,
|
||||||
defaults=track_defaults,
|
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
|
# optimisation for reducing SQL queries, because we cannot use select_related with
|
||||||
# update or create, so we restore the cache by hand
|
# update or create, so we restore the cache by hand
|
||||||
if existing_track:
|
if existing_track:
|
||||||
|
|
|
@ -27,7 +27,7 @@ ARTIST_PREFETCH_QS = (
|
||||||
"attachment_cover",
|
"attachment_cover",
|
||||||
)
|
)
|
||||||
.prefetch_related(music_views.TAG_PREFETCH)
|
.prefetch_related(music_views.TAG_PREFETCH)
|
||||||
.annotate(_tracks_count=Count("tracks"))
|
.annotate(_tracks_count=Count("artist_credit__tracks"))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ class ChannelViewSet(
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.action == "retrieve":
|
if self.action == "retrieve":
|
||||||
queryset = queryset.annotate(
|
queryset = queryset.annotate(
|
||||||
_downloads_count=Sum("artist__tracks__downloads_count")
|
_downloads_count=Sum("artist__artist_credit__tracks__downloads_count")
|
||||||
)
|
)
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
@ -192,7 +192,6 @@ class ChannelViewSet(
|
||||||
if object.attributed_to == actors.get_service_actor():
|
if object.attributed_to == actors.get_service_actor():
|
||||||
# external feed, we redirect to the canonical one
|
# external feed, we redirect to the canonical one
|
||||||
return http.HttpResponseRedirect(object.rss_url)
|
return http.HttpResponseRedirect(object.rss_url)
|
||||||
|
|
||||||
uploads = (
|
uploads = (
|
||||||
object.library.uploads.playable_by(None)
|
object.library.uploads.playable_by(None)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
|
|
|
@ -68,22 +68,33 @@ def create_taggable_items(dependency):
|
||||||
|
|
||||||
|
|
||||||
CONFIG = [
|
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",
|
"id": "tracks",
|
||||||
"model": music_models.Track,
|
"model": music_models.Track,
|
||||||
"factory": "music.Track",
|
"factory": "music.Track",
|
||||||
"factory_kwargs": {"artist": None, "album": None},
|
"factory_kwargs": {"album": None},
|
||||||
"depends_on": [
|
"depends_on": [
|
||||||
{"field": "album", "id": "albums", "default_factor": 0.1},
|
{"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",
|
"id": "albums",
|
||||||
"model": music_models.Album,
|
"model": music_models.Album,
|
||||||
"factory": "music.Album",
|
"factory": "music.Album",
|
||||||
"factory_kwargs": {"artist": None},
|
"factory_kwargs": {},
|
||||||
"depends_on": [{"field": "artist", "id": "artists", "default_factor": 0.3}],
|
"depends_on": [
|
||||||
|
{"field": "artist_credit", "id": "artist_credit", "default_factor": 0.3}
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{"id": "artists", "model": music_models.Artist, "factory": "music.Artist"},
|
{"id": "artists", "model": music_models.Artist, "factory": "music.Artist"},
|
||||||
{
|
{
|
||||||
|
@ -310,12 +321,23 @@ class Command(BaseCommand):
|
||||||
candidates = list(queryset.values_list("pk", flat=True))
|
candidates = list(queryset.values_list("pk", flat=True))
|
||||||
picked_pks = [random.choice(candidates) for _ in objects]
|
picked_pks = [random.choice(candidates) for _ in objects]
|
||||||
picked_objects = {o.pk: o for o in queryset.filter(pk__in=picked_pks)}
|
picked_objects = {o.pk: o for o in queryset.filter(pk__in=picked_pks)}
|
||||||
|
|
||||||
|
saved_obj = []
|
||||||
for i, obj in enumerate(objects):
|
for i, obj in enumerate(objects):
|
||||||
if create_dependencies:
|
if create_dependencies:
|
||||||
value = random.choice(candidates)
|
value = random.choice(candidates)
|
||||||
else:
|
else:
|
||||||
value = picked_objects[picked_pks[i]]
|
value = picked_objects[picked_pks[i]]
|
||||||
setattr(obj, dependency["field"], value)
|
if dependency["field"] == "artist_credit":
|
||||||
|
obj.save()
|
||||||
|
obj.artist_credit.set([value])
|
||||||
|
saved_obj.append(obj)
|
||||||
|
|
||||||
|
else:
|
||||||
|
setattr(obj, dependency["field"], value)
|
||||||
|
if saved_obj:
|
||||||
|
return saved_obj
|
||||||
|
|
||||||
if not handler:
|
if not handler:
|
||||||
objects = row["model"].objects.bulk_create(objects, batch_size=BATCH_SIZE)
|
objects = row["model"].objects.bulk_create(objects, batch_size=BATCH_SIZE)
|
||||||
results[row["id"]] = objects
|
results[row["id"]] = objects
|
||||||
|
|
|
@ -43,9 +43,9 @@ def get_lb_listen(listening):
|
||||||
release_name = track.album.title
|
release_name = track.album.title
|
||||||
if track.album.mbid:
|
if track.album.mbid:
|
||||||
additional_info["release_mbid"] = str(track.album.mbid)
|
additional_info["release_mbid"] = str(track.album.mbid)
|
||||||
|
mbids = [ac.artist.mbid for ac in track.artist_credit.all() if ac.artist.mbid]
|
||||||
if track.artist.mbid:
|
if mbids:
|
||||||
additional_info["artist_mbids"] = [str(track.artist.mbid)]
|
additional_info["artist_mbids"] = mbids
|
||||||
|
|
||||||
upload = track.uploads.filter(duration__gte=0).first()
|
upload = track.uploads.filter(duration__gte=0).first()
|
||||||
if upload:
|
if upload:
|
||||||
|
@ -53,8 +53,8 @@ def get_lb_listen(listening):
|
||||||
|
|
||||||
return liblistenbrainz.Listen(
|
return liblistenbrainz.Listen(
|
||||||
track_name=track.title,
|
track_name=track.title,
|
||||||
artist_name=track.artist.name,
|
|
||||||
listened_at=listening.creation_date.timestamp(),
|
listened_at=listening.creation_date.timestamp(),
|
||||||
|
artist_name=track.get_artist_credit_string,
|
||||||
release_name=release_name,
|
release_name=release_name,
|
||||||
additional_info=additional_info,
|
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
|
# See https://github.com/krateng/maloja/blob/master/API.md
|
||||||
payload = {
|
payload = {
|
||||||
"key": api_key,
|
"key": api_key,
|
||||||
"artists": [track.artist.name],
|
"artists": [artist.name for artist in track.artist_credit.get_artists_list()],
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
"time": int(listening.creation_date.timestamp()),
|
"time": int(listening.creation_date.timestamp()),
|
||||||
"nofix": bool(conf.get("nofix")),
|
"nofix": bool(conf.get("nofix")),
|
||||||
|
@ -46,8 +46,10 @@ def get_payload(listening, api_key, conf):
|
||||||
if track.album:
|
if track.album:
|
||||||
if track.album.title:
|
if track.album.title:
|
||||||
payload["album"] = track.album.title
|
payload["album"] = track.album.title
|
||||||
if track.album.artist:
|
if track.album.artist_credit.all():
|
||||||
payload["albumartists"] = [track.album.artist.name]
|
payload["albumartists"] = [
|
||||||
|
artist.name for artist in track.album.artist_credit.get_artists_list()
|
||||||
|
]
|
||||||
|
|
||||||
upload = track.uploads.filter(duration__gte=0).first()
|
upload = track.uploads.filter(duration__gte=0).first()
|
||||||
if upload:
|
if upload:
|
||||||
|
|
|
@ -84,7 +84,7 @@ def get_scrobble_payload(track, date, suffix="[0]"):
|
||||||
"""
|
"""
|
||||||
upload = track.uploads.filter(duration__gte=0).first()
|
upload = track.uploads.filter(duration__gte=0).first()
|
||||||
data = {
|
data = {
|
||||||
f"a{suffix}": track.artist.name,
|
f"a{suffix}": track.get_artist_credit_string,
|
||||||
f"t{suffix}": track.title,
|
f"t{suffix}": track.title,
|
||||||
f"l{suffix}": upload.duration if upload else 0,
|
f"l{suffix}": upload.duration if upload else 0,
|
||||||
f"b{suffix}": (track.album.title if track.album else "") or "",
|
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()
|
upload = track.uploads.filter(duration__gte=0).first()
|
||||||
data = {
|
data = {
|
||||||
"artist": track.artist.name,
|
"artist": track.get_artist_credit_string,
|
||||||
"track": track.title,
|
"track": track.title,
|
||||||
"chosenByUser": 1,
|
"chosenByUser": 1,
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,11 +60,20 @@ class TrackFavoriteViewSet(
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
fields.privacy_level_query(self.request.user, "user__privacy_level")
|
fields.privacy_level_query(self.request.user, "user__privacy_level")
|
||||||
)
|
)
|
||||||
tracks = Track.objects.with_playable_uploads(
|
tracks = (
|
||||||
music_utils.get_actor_from_request(self.request)
|
Track.objects.with_playable_uploads(
|
||||||
).select_related(
|
music_utils.get_actor_from_request(self.request)
|
||||||
"artist", "album__artist", "attributed_to", "album__attachment_cover"
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"artist_credit__artist",
|
||||||
|
"album__artist_credit__artist",
|
||||||
|
)
|
||||||
|
.select_related(
|
||||||
|
"attributed_to",
|
||||||
|
"album__attachment_cover",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
queryset = queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
|
|
|
@ -293,6 +293,7 @@ CONTEXTS = [
|
||||||
"Album": "fw:Album",
|
"Album": "fw:Album",
|
||||||
"Track": "fw:Track",
|
"Track": "fw:Track",
|
||||||
"Artist": "fw:Artist",
|
"Artist": "fw:Artist",
|
||||||
|
"ArtistCredit": "fw:ArtistCredit",
|
||||||
"Library": "fw:Library",
|
"Library": "fw:Library",
|
||||||
"bitrate": {"@id": "fw:bitrate", "@type": "xsd:nonNegativeInteger"},
|
"bitrate": {"@id": "fw:bitrate", "@type": "xsd:nonNegativeInteger"},
|
||||||
"size": {"@id": "fw:size", "@type": "xsd:nonNegativeInteger"},
|
"size": {"@id": "fw:size", "@type": "xsd:nonNegativeInteger"},
|
||||||
|
@ -302,7 +303,16 @@ CONTEXTS = [
|
||||||
"track": {"@id": "fw:track", "@type": "@id"},
|
"track": {"@id": "fw:track", "@type": "@id"},
|
||||||
"cover": {"@id": "fw:cover", "@type": "as:Link"},
|
"cover": {"@id": "fw:cover", "@type": "as:Link"},
|
||||||
"album": {"@id": "fw:album", "@type": "@id"},
|
"album": {"@id": "fw:album", "@type": "@id"},
|
||||||
|
"artist": {"@id": "fw:artist", "@type": "@id"},
|
||||||
"artists": {"@id": "fw:artists", "@type": "@id", "@container": "@list"},
|
"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"},
|
||||||
|
"credit": {"@id": "fw:credit", "@type": "xsd:string"},
|
||||||
|
"index": {"@id": "fw:index", "@type": "xsd:nonNegativeInteger"},
|
||||||
"released": {"@id": "fw:released", "@type": "xsd:date"},
|
"released": {"@id": "fw:released", "@type": "xsd:date"},
|
||||||
"musicbrainzId": "fw:musicbrainzId",
|
"musicbrainzId": "fw:musicbrainzId",
|
||||||
"license": {"@id": "fw:license", "@type": "@id"},
|
"license": {"@id": "fw:license", "@type": "@id"},
|
||||||
|
|
|
@ -191,7 +191,6 @@ def prepare_for_serializer(payload, config, fallbacks={}):
|
||||||
value = noop
|
value = noop
|
||||||
if not aliases:
|
if not aliases:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
for a in aliases:
|
for a in aliases:
|
||||||
try:
|
try:
|
||||||
value = get_value(
|
value = get_value(
|
||||||
|
@ -279,7 +278,6 @@ class JsonLdSerializer(serializers.Serializer):
|
||||||
for field in dereferenced_fields:
|
for field in dereferenced_fields:
|
||||||
for i in get_ids(data[field]):
|
for i in get_ids(data[field]):
|
||||||
dereferenced_ids.add(i)
|
dereferenced_ids.add(i)
|
||||||
|
|
||||||
if dereferenced_ids:
|
if dereferenced_ids:
|
||||||
try:
|
try:
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
|
@ -293,7 +293,7 @@ def inbox_delete_audio(payload, context):
|
||||||
upload_fids = [payload["object"]["id"]]
|
upload_fids = [payload["object"]["id"]]
|
||||||
|
|
||||||
query = Q(fid__in=upload_fids) & (
|
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)
|
candidates = music_models.Upload.objects.filter(query)
|
||||||
|
|
||||||
|
@ -577,7 +577,9 @@ def inbox_delete_album(payload, context):
|
||||||
logger.debug("Discarding deletion of empty library")
|
logger.debug("Discarding deletion of empty library")
|
||||||
return
|
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:
|
try:
|
||||||
album = music_models.Album.objects.get(query)
|
album = music_models.Album.objects.get(query)
|
||||||
except music_models.Album.DoesNotExist:
|
except music_models.Album.DoesNotExist:
|
||||||
|
@ -590,9 +592,10 @@ def inbox_delete_album(payload, context):
|
||||||
@outbox.register({"type": "Delete", "object.type": "Album"})
|
@outbox.register({"type": "Delete", "object.type": "Album"})
|
||||||
def outbox_delete_album(context):
|
def outbox_delete_album(context):
|
||||||
album = context["album"]
|
album = context["album"]
|
||||||
|
album_artist = album.artist_credit.all()[0].artist
|
||||||
actor = (
|
actor = (
|
||||||
album.artist.channel.actor
|
album_artist.channel.actor
|
||||||
if album.artist.get_channel()
|
if album_artist.get_channel()
|
||||||
else album.attributed_to
|
else album.attributed_to
|
||||||
)
|
)
|
||||||
actor = actor or actors.get_service_actor()
|
actor = actor or actors.get_service_actor()
|
||||||
|
|
|
@ -6,6 +6,7 @@ import uuid
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
@ -1221,12 +1222,22 @@ class MusicEntitySerializer(jsonld.JsonLdSerializer):
|
||||||
self.updateable_fields, validated_data, instance
|
self.updateable_fields, validated_data, instance
|
||||||
)
|
)
|
||||||
updated_fields = self.validate_updated_data(instance, updated_fields)
|
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:
|
if creating:
|
||||||
instance, created = self.Meta.model.objects.get_or_create(
|
instance, created = self.Meta.model.objects.get_or_create(
|
||||||
fid=validated_data["id"], defaults=updated_fields
|
fid=validated_data["id"], defaults=updated_fields
|
||||||
)
|
)
|
||||||
|
if set_ac:
|
||||||
|
instance.artist_credit.set(artist_credit)
|
||||||
else:
|
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 = [t["name"] for t in validated_data.get("tags", []) or []]
|
||||||
tags_models.set_tags(instance, *tags)
|
tags_models.set_tags(instance, *tags)
|
||||||
|
@ -1288,7 +1299,6 @@ class ArtistSerializer(MusicEntitySerializer):
|
||||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||||
{
|
{
|
||||||
"released": jsonld.first_val(contexts.FW.released),
|
"released": jsonld.first_val(contexts.FW.released),
|
||||||
"artists": jsonld.first_attr(contexts.FW.artists, "@list"),
|
|
||||||
"image": jsonld.first_obj(contexts.AS.image),
|
"image": jsonld.first_obj(contexts.AS.image),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -1314,12 +1324,53 @@ class ArtistSerializer(MusicEntitySerializer):
|
||||||
create = MusicEntitySerializer.update_or_create
|
create = MusicEntitySerializer.update_or_create
|
||||||
|
|
||||||
|
|
||||||
|
class ArtistCreditSerializer(jsonld.JsonLdSerializer):
|
||||||
|
artist = 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
|
||||||
|
)
|
||||||
|
published = serializers.DateTimeField()
|
||||||
|
id = serializers.URLField(max_length=500)
|
||||||
|
|
||||||
|
updateable_fields = [
|
||||||
|
("credit", "credit"),
|
||||||
|
("artist", "artist"),
|
||||||
|
("joinphrase", "joinphrase"),
|
||||||
|
]
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.ArtistCredit
|
||||||
|
jsonld_mapping = {
|
||||||
|
"artist": jsonld.first_obj(contexts.FW.artist),
|
||||||
|
"credit": jsonld.first_val(contexts.FW.credit),
|
||||||
|
"index": jsonld.first_val(contexts.FW.index),
|
||||||
|
"joinphrase": jsonld.first_val(contexts.FW.joinphrase),
|
||||||
|
"published": jsonld.first_val(contexts.AS.published),
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_representation(self, instance):
|
||||||
|
data = {
|
||||||
|
"type": "ArtistCredit",
|
||||||
|
"id": instance.fid,
|
||||||
|
"artist": ArtistSerializer(
|
||||||
|
instance.artist, context={"include_ap_context": False}
|
||||||
|
).data,
|
||||||
|
"joinphrase": instance.joinphrase,
|
||||||
|
"credit": instance.credit,
|
||||||
|
"index": instance.index,
|
||||||
|
"published": instance.creation_date.isoformat(),
|
||||||
|
}
|
||||||
|
if self.context.get("include_ap_context", self.parent is None):
|
||||||
|
data["@context"] = jsonld.get_default_context()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(MusicEntitySerializer):
|
class AlbumSerializer(MusicEntitySerializer):
|
||||||
released = serializers.DateField(allow_null=True, required=False)
|
released = serializers.DateField(allow_null=True, required=False)
|
||||||
artists = serializers.ListField(
|
artist_credit = serializers.ListField(child=ArtistCreditSerializer(), min_length=1)
|
||||||
child=MultipleSerializer(allowed=[BasicActorSerializer, ArtistSerializer]),
|
|
||||||
min_length=1,
|
|
||||||
)
|
|
||||||
image = ImageSerializer(
|
image = ImageSerializer(
|
||||||
allowed_mimetypes=["image/*"],
|
allowed_mimetypes=["image/*"],
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
|
@ -1332,7 +1383,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
||||||
("musicbrainzId", "mbid"),
|
("musicbrainzId", "mbid"),
|
||||||
("attributedTo", "attributed_to"),
|
("attributedTo", "attributed_to"),
|
||||||
("released", "release_date"),
|
("released", "release_date"),
|
||||||
("_artist", "artist"),
|
("artist_credit", "artist_credit"),
|
||||||
]
|
]
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -1341,13 +1392,13 @@ class AlbumSerializer(MusicEntitySerializer):
|
||||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||||
{
|
{
|
||||||
"released": jsonld.first_val(contexts.FW.released),
|
"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),
|
"image": jsonld.first_obj(contexts.AS.image),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
d = {
|
data = {
|
||||||
"type": "Album",
|
"type": "Album",
|
||||||
"id": instance.fid,
|
"id": instance.fid,
|
||||||
"name": instance.title,
|
"name": instance.title,
|
||||||
|
@ -1361,42 +1412,40 @@ class AlbumSerializer(MusicEntitySerializer):
|
||||||
else None,
|
else None,
|
||||||
"tag": self.get_tags_repr(instance),
|
"tag": self.get_tags_repr(instance),
|
||||||
}
|
}
|
||||||
if instance.artist.get_channel():
|
|
||||||
d["artists"] = [
|
data["artist_credit"] = ArtistCreditSerializer(
|
||||||
{
|
instance.artist_credit.all(),
|
||||||
"type": instance.artist.channel.actor.type,
|
context={"include_ap_context": False},
|
||||||
"id": instance.artist.channel.actor.fid,
|
many=True,
|
||||||
}
|
).data
|
||||||
]
|
include_content(data, instance.description)
|
||||||
else:
|
|
||||||
d["artists"] = [
|
|
||||||
ArtistSerializer(
|
|
||||||
instance.artist, context={"include_ap_context": False}
|
|
||||||
).data
|
|
||||||
]
|
|
||||||
include_content(d, instance.description)
|
|
||||||
if instance.attachment_cover:
|
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):
|
if self.context.get("include_ap_context", self.parent is None):
|
||||||
d["@context"] = jsonld.get_default_context()
|
data["@context"] = jsonld.get_default_context()
|
||||||
return d
|
return data
|
||||||
|
|
||||||
def validate(self, data):
|
def validate(self, data):
|
||||||
validated_data = super().validate(data)
|
validated_data = super().validate(data)
|
||||||
if not self.parent:
|
if not self.parent:
|
||||||
artist_data = validated_data["artists"][0]
|
artist_credit_data = validated_data["artist_credit"]
|
||||||
if artist_data.get("type", "Artist") == "Artist":
|
if artist_credit_data[0]["artist"].get("type", "Artist") == "Artist":
|
||||||
validated_data["_artist"] = utils.retrieve_ap_object(
|
acs = []
|
||||||
artist_data["id"],
|
for ac in validated_data["artist_credit"]:
|
||||||
actor=self.context.get("fetch_actor"),
|
acs.append(
|
||||||
queryset=music_models.Artist,
|
utils.retrieve_ap_object(
|
||||||
serializer_class=ArtistSerializer,
|
ac["id"],
|
||||||
)
|
actor=self.context.get("fetch_actor"),
|
||||||
|
queryset=music_models.ArtistCredit,
|
||||||
|
serializer_class=ArtistCreditSerializer,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
validated_data["artist_credit"] = acs
|
||||||
else:
|
else:
|
||||||
# we have an actor as an artist, so it's a channel
|
# we have an actor as an artist, so it's a channel
|
||||||
actor = actors.get_actor(artist_data["id"])
|
actor = actors.get_actor(artist_credit_data[0]["artist"]["id"])
|
||||||
validated_data["_artist"] = actor.channel.artist
|
validated_data["artist_credit"] = [{"artist": actor.channel.artist}]
|
||||||
|
|
||||||
return validated_data
|
return validated_data
|
||||||
|
|
||||||
|
@ -1406,7 +1455,7 @@ class AlbumSerializer(MusicEntitySerializer):
|
||||||
class TrackSerializer(MusicEntitySerializer):
|
class TrackSerializer(MusicEntitySerializer):
|
||||||
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
|
position = serializers.IntegerField(min_value=0, allow_null=True, required=False)
|
||||||
disc = serializers.IntegerField(min_value=1, 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()
|
album = AlbumSerializer()
|
||||||
license = serializers.URLField(allow_null=True, required=False)
|
license = serializers.URLField(allow_null=True, required=False)
|
||||||
copyright = serializers.CharField(allow_null=True, required=False)
|
copyright = serializers.CharField(allow_null=True, required=False)
|
||||||
|
@ -1434,7 +1483,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
MUSIC_ENTITY_JSONLD_MAPPING,
|
MUSIC_ENTITY_JSONLD_MAPPING,
|
||||||
{
|
{
|
||||||
"album": jsonld.first_obj(contexts.FW.album),
|
"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),
|
"copyright": jsonld.first_val(contexts.FW.copyright),
|
||||||
"disc": jsonld.first_val(contexts.FW.disc),
|
"disc": jsonld.first_val(contexts.FW.disc),
|
||||||
"license": jsonld.first_id(contexts.FW.license),
|
"license": jsonld.first_id(contexts.FW.license),
|
||||||
|
@ -1444,7 +1493,7 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
d = {
|
data = {
|
||||||
"type": "Track",
|
"type": "Track",
|
||||||
"id": instance.fid,
|
"id": instance.fid,
|
||||||
"name": instance.title,
|
"name": instance.title,
|
||||||
|
@ -1456,11 +1505,11 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
if instance.local_license
|
if instance.local_license
|
||||||
else None,
|
else None,
|
||||||
"copyright": instance.copyright if instance.copyright else None,
|
"copyright": instance.copyright if instance.copyright else None,
|
||||||
"artists": [
|
"artist_credit": ArtistCreditSerializer(
|
||||||
ArtistSerializer(
|
instance.artist_credit.all(),
|
||||||
instance.artist, context={"include_ap_context": False}
|
context={"include_ap_context": False},
|
||||||
).data
|
many=True,
|
||||||
],
|
).data,
|
||||||
"album": AlbumSerializer(
|
"album": AlbumSerializer(
|
||||||
instance.album, context={"include_ap_context": False}
|
instance.album, context={"include_ap_context": False}
|
||||||
).data,
|
).data,
|
||||||
|
@ -1469,12 +1518,13 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
else None,
|
else None,
|
||||||
"tag": self.get_tags_repr(instance),
|
"tag": self.get_tags_repr(instance),
|
||||||
}
|
}
|
||||||
include_content(d, instance.description)
|
include_content(data, instance.description)
|
||||||
include_image(d, instance.attachment_cover)
|
include_image(data, instance.attachment_cover)
|
||||||
if self.context.get("include_ap_context", self.parent is None):
|
if self.context.get("include_ap_context", self.parent is None):
|
||||||
d["@context"] = jsonld.get_default_context()
|
data["@context"] = jsonld.get_default_context()
|
||||||
return d
|
return data
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
from funkwhale_api.music import tasks as music_tasks
|
from funkwhale_api.music import tasks as music_tasks
|
||||||
|
|
||||||
|
@ -1490,18 +1540,21 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
validated_data, "album.attributedTo", permissive=True
|
validated_data, "album.attributedTo", permissive=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
artists = (
|
artist_credit = (
|
||||||
common_utils.recursive_getattr(validated_data, "artists", permissive=True)
|
|
||||||
or []
|
|
||||||
)
|
|
||||||
album_artists = (
|
|
||||||
common_utils.recursive_getattr(
|
common_utils.recursive_getattr(
|
||||||
validated_data, "album.artists", permissive=True
|
validated_data, "artist_credit", permissive=True
|
||||||
)
|
)
|
||||||
or []
|
or []
|
||||||
)
|
)
|
||||||
for artist in artists + album_artists:
|
album_artists_credit = (
|
||||||
actors_to_fetch.add(artist.get("attributedTo"))
|
common_utils.recursive_getattr(
|
||||||
|
validated_data, "album.artist_credit", permissive=True
|
||||||
|
)
|
||||||
|
or []
|
||||||
|
)
|
||||||
|
|
||||||
|
for ac in artist_credit + album_artists_credit:
|
||||||
|
actors_to_fetch.add(ac["artist"].get("attributedTo"))
|
||||||
|
|
||||||
for url in actors_to_fetch:
|
for url in actors_to_fetch:
|
||||||
if not url:
|
if not url:
|
||||||
|
@ -1514,8 +1567,9 @@ class TrackSerializer(MusicEntitySerializer):
|
||||||
from_activity = self.context.get("activity")
|
from_activity = self.context.get("activity")
|
||||||
if from_activity:
|
if from_activity:
|
||||||
metadata["from_activity_id"] = from_activity.pk
|
metadata["from_activity_id"] = from_activity.pk
|
||||||
track = music_tasks.get_track_from_import_metadata(metadata, update_cover=True)
|
track = music_tasks.get_track_from_import_metadata(
|
||||||
|
metadata, update_cover=True, query_mb=False
|
||||||
|
)
|
||||||
return track
|
return track
|
||||||
|
|
||||||
def update(self, obj, validated_data):
|
def update(self, obj, validated_data):
|
||||||
|
@ -1780,7 +1834,7 @@ class ChannelOutboxSerializer(PaginatedCollectionSerializer):
|
||||||
"actor": channel.actor,
|
"actor": channel.actor,
|
||||||
"items": channel.library.uploads.for_federation()
|
"items": channel.library.uploads.for_federation()
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
.filter(track__artist=channel.artist),
|
.filter(track__artist_credit__artist=channel.artist),
|
||||||
"type": "OrderedCollection",
|
"type": "OrderedCollection",
|
||||||
}
|
}
|
||||||
r = super().to_representation(conf)
|
r = super().to_representation(conf)
|
||||||
|
@ -1850,7 +1904,7 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
||||||
actor=actors.get_service_actor(),
|
actor=actors.get_service_actor(),
|
||||||
serializer_class=AlbumSerializer,
|
serializer_class=AlbumSerializer,
|
||||||
queryset=music_models.Album.objects.filter(
|
queryset=music_models.Album.objects.filter(
|
||||||
artist__channel=self.context["channel"]
|
artist_credit__artist__channel=self.context["channel"]
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1929,7 +1983,6 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
||||||
now = timezone.now()
|
now = timezone.now()
|
||||||
track_defaults = {
|
track_defaults = {
|
||||||
"fid": validated_data["id"],
|
"fid": validated_data["id"],
|
||||||
"artist": channel.artist,
|
|
||||||
"position": validated_data.get("position", 1),
|
"position": validated_data.get("position", 1),
|
||||||
"disc_number": validated_data.get("disc", 1),
|
"disc_number": validated_data.get("disc", 1),
|
||||||
"title": validated_data["name"],
|
"title": validated_data["name"],
|
||||||
|
@ -1942,9 +1995,32 @@ class ChannelUploadSerializer(jsonld.JsonLdSerializer):
|
||||||
track_defaults["license"] = licenses.match(validated_data["license"])
|
track_defaults["license"] = licenses.match(validated_data["license"])
|
||||||
|
|
||||||
track, created = music_models.Track.objects.update_or_create(
|
track, created = music_models.Track.objects.update_or_create(
|
||||||
artist__channel=channel, fid=validated_data["id"], defaults=track_defaults
|
fid=validated_data["id"],
|
||||||
|
defaults=track_defaults,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# only one artist_credit per channel
|
||||||
|
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:
|
if "image" in validated_data:
|
||||||
new_value = self.validated_data["image"]
|
new_value = self.validated_data["image"]
|
||||||
common_utils.attach_file(
|
common_utils.attach_file(
|
||||||
|
|
|
@ -17,6 +17,7 @@ router.register(r".well-known", views.WellKnownViewSet, "well-known")
|
||||||
music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries")
|
music_router.register(r"libraries", views.MusicLibraryViewSet, "libraries")
|
||||||
music_router.register(r"uploads", views.MusicUploadViewSet, "uploads")
|
music_router.register(r"uploads", views.MusicUploadViewSet, "uploads")
|
||||||
music_router.register(r"artists", views.MusicArtistViewSet, "artists")
|
music_router.register(r"artists", views.MusicArtistViewSet, "artists")
|
||||||
|
music_router.register(r"artistcredit", views.MusicArtistCreditViewSet, "artistcredit")
|
||||||
music_router.register(r"albums", views.MusicAlbumViewSet, "albums")
|
music_router.register(r"albums", views.MusicAlbumViewSet, "albums")
|
||||||
music_router.register(r"tracks", views.MusicTrackViewSet, "tracks")
|
music_router.register(r"tracks", views.MusicTrackViewSet, "tracks")
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,9 @@ class ActorViewSet(FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericV
|
||||||
"actor": channel.actor,
|
"actor": channel.actor,
|
||||||
"items": channel.library.uploads.for_federation()
|
"items": channel.library.uploads.for_federation()
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
.prefetch_related("library__channel__actor", "track__artist"),
|
.prefetch_related(
|
||||||
|
"library__channel__actor", "track__artist_credit__artist"
|
||||||
|
),
|
||||||
"item_serializer": serializers.ChannelCreateUploadSerializer,
|
"item_serializer": serializers.ChannelCreateUploadSerializer,
|
||||||
}
|
}
|
||||||
return get_collection_response(
|
return get_collection_response(
|
||||||
|
@ -290,21 +292,21 @@ class MusicLibraryViewSet(
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"track",
|
"track",
|
||||||
queryset=music_models.Track.objects.select_related(
|
queryset=music_models.Track.objects.select_related(
|
||||||
"album__artist__attributed_to",
|
|
||||||
"artist__attributed_to",
|
|
||||||
"artist__attachment_cover",
|
|
||||||
"attachment_cover",
|
"attachment_cover",
|
||||||
"album__attributed_to",
|
"album__attributed_to",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
"album__attachment_cover",
|
"album__attachment_cover",
|
||||||
"album__artist__attachment_cover",
|
|
||||||
"description",
|
"description",
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
|
"album__artist_credit__artist__attributed_to",
|
||||||
|
"artist_credit__artist__attributed_to",
|
||||||
|
"artist_credit__artist__attachment_cover",
|
||||||
"tagged_items__tag",
|
"tagged_items__tag",
|
||||||
"album__tagged_items__tag",
|
"album__tagged_items__tag",
|
||||||
"album__artist__tagged_items__tag",
|
"album__artist_credit__artist__tagged_items__tag",
|
||||||
"artist__tagged_items__tag",
|
"album__artist_credit__artist__attachment_cover",
|
||||||
"artist__description",
|
"artist_credit__artist__tagged_items__tag",
|
||||||
|
"artist_credit__artist__description",
|
||||||
"album__description",
|
"album__description",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -331,15 +333,20 @@ class MusicUploadViewSet(
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Upload.objects.local().select_related(
|
queryset = (
|
||||||
"library__actor",
|
music_models.Upload.objects.local()
|
||||||
"track__artist",
|
.select_related(
|
||||||
"track__album__artist",
|
"library__actor",
|
||||||
"track__description",
|
"track__description",
|
||||||
"track__album__attachment_cover",
|
"track__album__attachment_cover",
|
||||||
"track__album__artist__attachment_cover",
|
"track__attachment_cover",
|
||||||
"track__artist__attachment_cover",
|
)
|
||||||
"track__attachment_cover",
|
.prefetch_related(
|
||||||
|
"track__artist_credit__artist",
|
||||||
|
"track__album__artist_credit__artist",
|
||||||
|
"track__album__artist_credit__artist__attachment_cover",
|
||||||
|
"track__artist_credit__artist__attachment_cover",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.UploadSerializer
|
serializer_class = serializers.UploadSerializer
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
@ -393,13 +400,35 @@ class MusicArtistViewSet(
|
||||||
return response.Response(serializer.data)
|
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()
|
||||||
|
serializer = self.get_serializer(instance)
|
||||||
|
return response.Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
class MusicAlbumViewSet(
|
class MusicAlbumViewSet(
|
||||||
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
FederationMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Album.objects.local().select_related(
|
queryset = (
|
||||||
"artist__description", "description", "artist__attachment_cover"
|
music_models.Album.objects.local()
|
||||||
|
.prefetch_related(
|
||||||
|
"artist_credit__artist__description",
|
||||||
|
"artist_credit__artist__attachment_cover",
|
||||||
|
)
|
||||||
|
.select_related(
|
||||||
|
"description",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.AlbumSerializer
|
serializer_class = serializers.AlbumSerializer
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
@ -418,16 +447,22 @@ class MusicTrackViewSet(
|
||||||
):
|
):
|
||||||
authentication_classes = [authentication.SignatureAuthentication]
|
authentication_classes = [authentication.SignatureAuthentication]
|
||||||
renderer_classes = renderers.get_ap_renderers()
|
renderer_classes = renderers.get_ap_renderers()
|
||||||
queryset = music_models.Track.objects.local().select_related(
|
queryset = (
|
||||||
"album__artist",
|
music_models.Track.objects.local()
|
||||||
"album__description",
|
.select_related(
|
||||||
"artist__description",
|
"album__description",
|
||||||
"description",
|
"description",
|
||||||
"attachment_cover",
|
"attachment_cover",
|
||||||
"album__artist__attachment_cover",
|
"album__attachment_cover",
|
||||||
"album__attachment_cover",
|
)
|
||||||
"artist__attachment_cover",
|
.prefetch_related(
|
||||||
|
"album__artist_credit__artist",
|
||||||
|
"artist_credit__artist__description",
|
||||||
|
"artist_credit__artist__attachment_cover",
|
||||||
|
"album__artist_credit__artist__attachment_cover",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer_class = serializers.TrackSerializer
|
serializer_class = serializers.TrackSerializer
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
|
||||||
|
|
|
@ -51,10 +51,16 @@ class ListeningViewSet(
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
fields.privacy_level_query(self.request.user, "user__privacy_level")
|
fields.privacy_level_query(self.request.user, "user__privacy_level")
|
||||||
)
|
)
|
||||||
tracks = Track.objects.with_playable_uploads(
|
tracks = (
|
||||||
music_utils.get_actor_from_request(self.request)
|
Track.objects.with_playable_uploads(
|
||||||
).select_related(
|
music_utils.get_actor_from_request(self.request)
|
||||||
"artist", "album__artist", "attributed_to", "artist__attachment_cover"
|
)
|
||||||
|
.prefetch_related(
|
||||||
|
"artist_credit",
|
||||||
|
"album__artist_credit__artist",
|
||||||
|
"artist_credit__artist__attachment_cover",
|
||||||
|
)
|
||||||
|
.select_related("attributed_to")
|
||||||
)
|
)
|
||||||
return queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
return queryset.prefetch_related(Prefetch("track", queryset=tracks))
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ def get_content():
|
||||||
|
|
||||||
def get_top_music_categories():
|
def get_top_music_categories():
|
||||||
return (
|
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)
|
.exclude(tagged_items__tag_id=None)
|
||||||
.values(name=F("tagged_items__tag__name"))
|
.values(name=F("tagged_items__tag__name"))
|
||||||
.annotate(count=Count("name"))
|
.annotate(count=Count("name"))
|
||||||
|
@ -47,7 +47,7 @@ def get_top_music_categories():
|
||||||
|
|
||||||
def get_top_podcast_categories():
|
def get_top_podcast_categories():
|
||||||
return (
|
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)
|
.exclude(tagged_items__tag_id=None)
|
||||||
.values(name=F("tagged_items__tag__name"))
|
.values(name=F("tagged_items__tag__name"))
|
||||||
.annotate(count=Count("name"))
|
.annotate(count=Count("name"))
|
||||||
|
|
|
@ -97,12 +97,15 @@ class ManageAlbumFilterSet(filters.FilterSet):
|
||||||
search_fields={
|
search_fields={
|
||||||
"title": {"to": "title"},
|
"title": {"to": "title"},
|
||||||
"fid": {"to": "fid"},
|
"fid": {"to": "fid"},
|
||||||
"artist": {"to": "artist__name"},
|
"artist": {"to": "artist_credit__artist__name"},
|
||||||
"mbid": {"to": "mbid"},
|
"mbid": {"to": "mbid"},
|
||||||
},
|
},
|
||||||
filter_fields={
|
filter_fields={
|
||||||
"uuid": {"to": "uuid"},
|
"uuid": {"to": "uuid"},
|
||||||
"artist_id": {"to": "artist_id", "field": forms.IntegerField()},
|
"artist_id": {
|
||||||
|
"to": "artist_credit__artist_id",
|
||||||
|
"field": forms.IntegerField(),
|
||||||
|
},
|
||||||
"domain": {
|
"domain": {
|
||||||
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
"handler": lambda v: federation_utils.get_domain_query_from_url(v)
|
||||||
},
|
},
|
||||||
|
@ -118,7 +121,7 @@ class ManageAlbumFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Album
|
model = music_models.Album
|
||||||
fields = ["title", "mbid", "fid", "artist"]
|
fields = ["title", "mbid", "fid", "artist_credit"]
|
||||||
|
|
||||||
|
|
||||||
class ManageTrackFilterSet(filters.FilterSet):
|
class ManageTrackFilterSet(filters.FilterSet):
|
||||||
|
@ -128,9 +131,9 @@ class ManageTrackFilterSet(filters.FilterSet):
|
||||||
"title": {"to": "title"},
|
"title": {"to": "title"},
|
||||||
"fid": {"to": "fid"},
|
"fid": {"to": "fid"},
|
||||||
"mbid": {"to": "mbid"},
|
"mbid": {"to": "mbid"},
|
||||||
"artist": {"to": "artist__name"},
|
"artist": {"to": "artist_credit__artist__name"},
|
||||||
"album": {"to": "album__title"},
|
"album": {"to": "album__title"},
|
||||||
"album_artist": {"to": "album__artist__name"},
|
"album_artist": {"to": "album__artist_credit__artist__name"},
|
||||||
"copyright": {"to": "copyright"},
|
"copyright": {"to": "copyright"},
|
||||||
},
|
},
|
||||||
filter_fields={
|
filter_fields={
|
||||||
|
@ -157,7 +160,7 @@ class ManageTrackFilterSet(filters.FilterSet):
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Track
|
model = music_models.Track
|
||||||
fields = ["title", "mbid", "fid", "artist", "album", "license"]
|
fields = ["title", "mbid", "fid", "artist_credit", "album", "license"]
|
||||||
|
|
||||||
|
|
||||||
class ManageLibraryFilterSet(filters.FilterSet):
|
class ManageLibraryFilterSet(filters.FilterSet):
|
||||||
|
|
|
@ -451,17 +451,25 @@ class ManageNestedArtistSerializer(ManageBaseArtistSerializer):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ManageNestedArtistCreditSerializer(ManageBaseArtistSerializer):
|
||||||
|
artist = ManageNestedArtistSerializer()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = music_models.ArtistCredit
|
||||||
|
fields = ["artist"]
|
||||||
|
|
||||||
|
|
||||||
class ManageAlbumSerializer(
|
class ManageAlbumSerializer(
|
||||||
music_serializers.OptionalDescriptionMixin, ManageBaseAlbumSerializer
|
music_serializers.OptionalDescriptionMixin, ManageBaseAlbumSerializer
|
||||||
):
|
):
|
||||||
attributed_to = ManageBaseActorSerializer()
|
attributed_to = ManageBaseActorSerializer()
|
||||||
artist = ManageNestedArtistSerializer()
|
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||||
tags = serializers.SerializerMethodField()
|
tags = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Album
|
model = music_models.Album
|
||||||
fields = ManageBaseAlbumSerializer.Meta.fields + [
|
fields = ManageBaseAlbumSerializer.Meta.fields + [
|
||||||
"artist",
|
"artist_credit",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
"tags",
|
"tags",
|
||||||
"tracks_count",
|
"tracks_count",
|
||||||
|
@ -477,17 +485,17 @@ class ManageAlbumSerializer(
|
||||||
|
|
||||||
|
|
||||||
class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
|
class ManageTrackAlbumSerializer(ManageBaseAlbumSerializer):
|
||||||
artist = ManageNestedArtistSerializer()
|
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Album
|
model = music_models.Album
|
||||||
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist"]
|
fields = ManageBaseAlbumSerializer.Meta.fields + ["artist_credit"]
|
||||||
|
|
||||||
|
|
||||||
class ManageTrackSerializer(
|
class ManageTrackSerializer(
|
||||||
music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer
|
music_serializers.OptionalDescriptionMixin, ManageNestedTrackSerializer
|
||||||
):
|
):
|
||||||
artist = ManageNestedArtistSerializer()
|
artist_credit = ManageNestedArtistCreditSerializer(many=True)
|
||||||
album = ManageTrackAlbumSerializer(allow_null=True)
|
album = ManageTrackAlbumSerializer(allow_null=True)
|
||||||
attributed_to = ManageBaseActorSerializer(allow_null=True)
|
attributed_to = ManageBaseActorSerializer(allow_null=True)
|
||||||
uploads_count = serializers.SerializerMethodField()
|
uploads_count = serializers.SerializerMethodField()
|
||||||
|
@ -497,7 +505,7 @@ class ManageTrackSerializer(
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Track
|
model = music_models.Track
|
||||||
fields = ManageNestedTrackSerializer.Meta.fields + [
|
fields = ManageNestedTrackSerializer.Meta.fields + [
|
||||||
"artist",
|
"artist_credit",
|
||||||
"album",
|
"album",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
"uploads_count",
|
"uploads_count",
|
||||||
|
|
|
@ -84,8 +84,8 @@ class ManageArtistViewSet(
|
||||||
music_models.Artist.objects.all()
|
music_models.Artist.objects.all()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to", "attachment_cover", "channel")
|
.select_related("attributed_to", "attachment_cover", "channel")
|
||||||
.annotate(_tracks_count=Count("tracks", distinct=True))
|
.annotate(_tracks_count=Count("artist_credit__tracks", distinct=True))
|
||||||
.annotate(_albums_count=Count("albums", distinct=True))
|
.annotate(_albums_count=Count("artist_credit__albums", distinct=True))
|
||||||
.prefetch_related(music_views.TAG_PREFETCH)
|
.prefetch_related(music_views.TAG_PREFETCH)
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ManageArtistSerializer
|
serializer_class = serializers.ManageArtistSerializer
|
||||||
|
@ -98,7 +98,7 @@ class ManageArtistViewSet(
|
||||||
def stats(self, request, *args, **kwargs):
|
def stats(self, request, *args, **kwargs):
|
||||||
artist = self.get_object()
|
artist = self.get_object()
|
||||||
tracks = music_models.Track.objects.filter(
|
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)
|
data = get_stats(tracks, artist)
|
||||||
return response.Response(data, status=200)
|
return response.Response(data, status=200)
|
||||||
|
@ -128,8 +128,8 @@ class ManageAlbumViewSet(
|
||||||
queryset = (
|
queryset = (
|
||||||
music_models.Album.objects.all()
|
music_models.Album.objects.all()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to", "artist", "attachment_cover")
|
.select_related("attributed_to", "attachment_cover")
|
||||||
.prefetch_related("tracks")
|
.prefetch_related("tracks", "artist_credit__artist")
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ManageAlbumSerializer
|
serializer_class = serializers.ManageAlbumSerializer
|
||||||
filterset_class = filters.ManageAlbumFilterSet
|
filterset_class = filters.ManageAlbumFilterSet
|
||||||
|
@ -177,10 +177,10 @@ class ManageTrackViewSet(
|
||||||
queryset = (
|
queryset = (
|
||||||
music_models.Track.objects.all()
|
music_models.Track.objects.all()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related(
|
.prefetch_related(
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
"artist",
|
"artist_credit",
|
||||||
"album__artist",
|
"album__artist_credit",
|
||||||
"album__attachment_cover",
|
"album__attachment_cover",
|
||||||
"attachment_cover",
|
"attachment_cover",
|
||||||
)
|
)
|
||||||
|
@ -273,11 +273,11 @@ class ManageLibraryViewSet(
|
||||||
)
|
)
|
||||||
artists = set(
|
artists = set(
|
||||||
music_models.Album.objects.filter(pk__in=albums).values_list(
|
music_models.Album.objects.filter(pk__in=albums).values_list(
|
||||||
"artist", flat=True
|
"artist_credit__artist", flat=True
|
||||||
)
|
)
|
||||||
) | set(
|
) | set(
|
||||||
music_models.Track.objects.filter(pk__in=tracks).values_list(
|
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 = (
|
queryset = (
|
||||||
music_models.Upload.objects.all()
|
music_models.Upload.objects.all()
|
||||||
.order_by("-id")
|
.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
|
serializer_class = serializers.ManageUploadSerializer
|
||||||
filterset_class = filters.ManageUploadFilterSet
|
filterset_class = filters.ManageUploadFilterSet
|
||||||
|
@ -703,8 +707,8 @@ class ManageChannelViewSet(
|
||||||
music_models.Artist.objects.all()
|
music_models.Artist.objects.all()
|
||||||
.order_by("-id")
|
.order_by("-id")
|
||||||
.select_related("attributed_to", "attachment_cover", "channel")
|
.select_related("attributed_to", "attachment_cover", "channel")
|
||||||
.annotate(_tracks_count=Count("tracks"))
|
.annotate(_tracks_count=Count("artist_credit__tracks"))
|
||||||
.annotate(_albums_count=Count("albums"))
|
.annotate(_albums_count=Count("artist_credit__albums"))
|
||||||
.prefetch_related(music_views.TAG_PREFETCH)
|
.prefetch_related(music_views.TAG_PREFETCH)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -720,7 +724,8 @@ class ManageChannelViewSet(
|
||||||
def stats(self, request, *args, **kwargs):
|
def stats(self, request, *args, **kwargs):
|
||||||
channel = self.get_object()
|
channel = self.get_object()
|
||||||
tracks = music_models.Track.objects.filter(
|
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 = get_stats(tracks, channel, ignore_fields=["libraries", "channels"])
|
||||||
data["follows"] = channel.actor.received_follows.count()
|
data["follows"] = channel.actor.received_follows.count()
|
||||||
|
|
|
@ -4,11 +4,24 @@ from django_filters import rest_framework as filters
|
||||||
USER_FILTER_CONFIG = {
|
USER_FILTER_CONFIG = {
|
||||||
"ARTIST": {"target_artist": ["pk"]},
|
"ARTIST": {"target_artist": ["pk"]},
|
||||||
"CHANNEL": {"target_artist": ["artist__pk"]},
|
"CHANNEL": {"target_artist": ["artist__pk"]},
|
||||||
"ALBUM": {"target_artist": ["artist__pk"]},
|
"ALBUM": {"target_artist": ["artist_credit__artist__pk"]},
|
||||||
"TRACK": {"target_artist": ["artist__pk", "album__artist__pk"]},
|
"TRACK": {
|
||||||
"LISTENING": {"target_artist": ["track__album__artist__pk", "track__artist__pk"]},
|
"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": {
|
"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")
|
@state_serializers.register(name="music.Album")
|
||||||
class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||||
tags = TAGS_FIELD
|
tags = TAGS_FIELD
|
||||||
artist = ArtistStateSerializer()
|
artist_credit = ArtistCreditStateSerializer(many=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Album
|
model = music_models.Album
|
||||||
|
@ -103,7 +122,7 @@ class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||||
"fid",
|
"fid",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"uuid",
|
"uuid",
|
||||||
"artist",
|
"artist_credit",
|
||||||
"release_date",
|
"release_date",
|
||||||
"tags",
|
"tags",
|
||||||
"description",
|
"description",
|
||||||
|
@ -113,7 +132,7 @@ class AlbumStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||||
@state_serializers.register(name="music.Track")
|
@state_serializers.register(name="music.Track")
|
||||||
class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||||
tags = TAGS_FIELD
|
tags = TAGS_FIELD
|
||||||
artist = ArtistStateSerializer()
|
artist_credit = ArtistCreditStateSerializer(many=True)
|
||||||
album = AlbumStateSerializer()
|
album = AlbumStateSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -125,7 +144,7 @@ class TrackStateSerializer(DescriptionStateMixin, serializers.ModelSerializer):
|
||||||
"fid",
|
"fid",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"uuid",
|
"uuid",
|
||||||
"artist",
|
"artist_credit",
|
||||||
"album",
|
"album",
|
||||||
"disc_number",
|
"disc_number",
|
||||||
"position",
|
"position",
|
||||||
|
@ -230,6 +249,7 @@ TARGET_CONFIG = {
|
||||||
"id_field": serializers.UUIDField(),
|
"id_field": serializers.UUIDField(),
|
||||||
},
|
},
|
||||||
"artist": {"queryset": music_models.Artist.objects.all()},
|
"artist": {"queryset": music_models.Artist.objects.all()},
|
||||||
|
"artist_credit": {"queryset": music_models.ArtistCredit.objects.all()},
|
||||||
"album": {"queryset": music_models.Album.objects.all()},
|
"album": {"queryset": music_models.Album.objects.all()},
|
||||||
"track": {"queryset": music_models.Track.objects.all()},
|
"track": {"queryset": music_models.Track.objects.all()},
|
||||||
"library": {
|
"library": {
|
||||||
|
|
|
@ -3,6 +3,17 @@ from funkwhale_api.common import admin
|
||||||
from . import models
|
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)
|
@admin.register(models.Artist)
|
||||||
class ArtistAdmin(admin.ModelAdmin):
|
class ArtistAdmin(admin.ModelAdmin):
|
||||||
list_display = ["name", "mbid", "creation_date", "modification_date"]
|
list_display = ["name", "mbid", "creation_date", "modification_date"]
|
||||||
|
@ -11,16 +22,18 @@ class ArtistAdmin(admin.ModelAdmin):
|
||||||
|
|
||||||
@admin.register(models.Album)
|
@admin.register(models.Album)
|
||||||
class AlbumAdmin(admin.ModelAdmin):
|
class AlbumAdmin(admin.ModelAdmin):
|
||||||
list_display = ["title", "artist", "mbid", "release_date", "creation_date"]
|
list_display = ["title", "mbid", "release_date", "creation_date"]
|
||||||
search_fields = ["title", "artist__name", "mbid"]
|
search_fields = ["title", "mbid"]
|
||||||
list_select_related = True
|
list_select_related = True
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.Track)
|
@admin.register(models.Track)
|
||||||
class TrackAdmin(admin.ModelAdmin):
|
class TrackAdmin(admin.ModelAdmin):
|
||||||
list_display = ["title", "artist", "album", "mbid"]
|
list_display = ["title", "album", "mbid", "artist"]
|
||||||
search_fields = ["title", "artist__name", "album__title", "mbid"]
|
search_fields = ["title", "album__title", "mbid"]
|
||||||
list_select_related = ["album__artist", "artist"]
|
|
||||||
|
def artist(self, obj):
|
||||||
|
return obj.get_artist_credit_string
|
||||||
|
|
||||||
|
|
||||||
@admin.register(models.TrackActor)
|
@admin.register(models.TrackActor)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
from django.forms import widgets
|
||||||
from dynamic_preferences import types
|
from dynamic_preferences import types
|
||||||
from dynamic_preferences.registries import global_preferences_registry
|
from dynamic_preferences.registries import global_preferences_registry
|
||||||
|
|
||||||
|
@ -159,3 +160,45 @@ class ReleaseDate(types.BooleanPreference):
|
||||||
verbose_name = "Release date Filter"
|
verbose_name = "Release date Filter"
|
||||||
help_text = "Only content with a release date will be displayed"
|
help_text = "Only content with a release date will be displayed"
|
||||||
default = False
|
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 the order or the values 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 "
|
||||||
|
r"(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 = (
|
||||||
|
r"featuring | feat\. | ft\. | feat | with | and | & | vs\. | \| | \||\| |\|| , | ,|, |,|"
|
||||||
|
r" ; | ;|; |;| 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 example: `artists = [artist1, Artist2]` will be displayed has : artist1.name, artis2.name"
|
||||||
|
"Changing this value will not update already parsed artists"
|
||||||
|
)
|
||||||
|
default = ", "
|
||||||
|
widget = widgets.Textarea
|
||||||
|
field_kwargs = {"required": False}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import factory
|
import factory
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from funkwhale_api.common import factories as common_factories
|
from funkwhale_api.common import factories as common_factories
|
||||||
from funkwhale_api.factories import NoUpdateOnCreate, registry
|
from funkwhale_api.factories import NoUpdateOnCreate, registry
|
||||||
|
@ -62,7 +64,7 @@ class ArtistFactory(
|
||||||
name = factory.Faker("name")
|
name = factory.Faker("name")
|
||||||
mbid = factory.Faker("uuid4")
|
mbid = factory.Faker("uuid4")
|
||||||
fid = factory.Faker("federation_url")
|
fid = factory.Faker("federation_url")
|
||||||
playable = playable_factory("track__album__artist")
|
playable = playable_factory("track__album__artist_credit__artist")
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "music.Artist"
|
model = "music.Artist"
|
||||||
|
@ -77,6 +79,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
|
@registry.register
|
||||||
class AlbumFactory(
|
class AlbumFactory(
|
||||||
tags_factories.TaggableFactory, NoUpdateOnCreate, factory.django.DjangoModelFactory
|
tags_factories.TaggableFactory, NoUpdateOnCreate, factory.django.DjangoModelFactory
|
||||||
|
@ -84,7 +96,6 @@ class AlbumFactory(
|
||||||
title = factory.Faker("sentence", nb_words=3)
|
title = factory.Faker("sentence", nb_words=3)
|
||||||
mbid = factory.Faker("uuid4")
|
mbid = factory.Faker("uuid4")
|
||||||
release_date = factory.Faker("date_object")
|
release_date = factory.Faker("date_object")
|
||||||
artist = factory.SubFactory(ArtistFactory)
|
|
||||||
release_group_id = factory.Faker("uuid4")
|
release_group_id = factory.Faker("uuid4")
|
||||||
fid = factory.Faker("federation_url")
|
fid = factory.Faker("federation_url")
|
||||||
playable = playable_factory("track__album")
|
playable = playable_factory("track__album")
|
||||||
|
@ -96,14 +107,22 @@ class AlbumFactory(
|
||||||
attributed = factory.Trait(
|
attributed = factory.Trait(
|
||||||
attributed_to=factory.SubFactory(federation_factories.ActorFactory)
|
attributed_to=factory.SubFactory(federation_factories.ActorFactory)
|
||||||
)
|
)
|
||||||
|
|
||||||
local = factory.Trait(
|
local = factory.Trait(
|
||||||
fid=factory.Faker("federation_url", local=True), artist__local=True
|
fid=factory.Faker("federation_url", local=True),
|
||||||
)
|
)
|
||||||
with_cover = factory.Trait(
|
with_cover = factory.Trait(
|
||||||
attachment_cover=factory.SubFactory(common_factories.AttachmentFactory)
|
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
|
@registry.register
|
||||||
class TrackFactory(
|
class TrackFactory(
|
||||||
|
@ -132,22 +151,29 @@ class TrackFactory(
|
||||||
)
|
)
|
||||||
|
|
||||||
@factory.post_generation
|
@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
|
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.
|
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)
|
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:
|
if extracted:
|
||||||
self.artist = extracted
|
self.artist_credit.add(extracted)
|
||||||
elif kwargs:
|
elif kwargs:
|
||||||
if created:
|
if created:
|
||||||
self.artist = ArtistFactory(**kwargs)
|
self.artist_credit.add(ArtistCreditFactory(**kwargs))
|
||||||
else:
|
else:
|
||||||
self.artist = ArtistFactory.build(**kwargs)
|
self.artist_credit.add(ArtistCreditFactory.build(**kwargs))
|
||||||
elif self.album:
|
elif self.album:
|
||||||
self.artist = self.album.artist
|
self.artist_credit.set(self.album.artist_credit.all())
|
||||||
if created:
|
if created:
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
|
@ -195,7 +221,9 @@ class UploadFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
from funkwhale_api.audio import factories as audio_factories
|
from funkwhale_api.audio import factories as audio_factories
|
||||||
|
|
||||||
audio_factories.ChannelFactory(
|
audio_factories.ChannelFactory(
|
||||||
library=self.library, artist=self.track.artist, **kwargs
|
library=self.library,
|
||||||
|
artist=self.track.artist_credit.all()[0].artist,
|
||||||
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -100,9 +100,9 @@ class ArtistFilter(
|
||||||
tag = TAG_FILTER
|
tag = TAG_FILTER
|
||||||
content_category = filters.CharFilter("content_category")
|
content_category = filters.CharFilter("content_category")
|
||||||
scope = common_filters.ActorScopeFilter(
|
scope = common_filters.ActorScopeFilter(
|
||||||
actor_field="tracks__uploads__library__actor",
|
actor_field="artist_credit__tracks__uploads__library__actor",
|
||||||
distinct=True,
|
distinct=True,
|
||||||
library_field="tracks__uploads__library",
|
library_field="artist_credit__tracks__uploads__library",
|
||||||
)
|
)
|
||||||
ordering = common_filters.CaseInsensitiveNameOrderingFilter(
|
ordering = common_filters.CaseInsensitiveNameOrderingFilter(
|
||||||
fields=(
|
fields=(
|
||||||
|
@ -128,14 +128,14 @@ class ArtistFilter(
|
||||||
}
|
}
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ARTIST"]
|
||||||
include_channels_field = "channel"
|
include_channels_field = "channel"
|
||||||
library_filter_field = "track__artist"
|
library_filter_field = "track__artist_credit__artist"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value).distinct()
|
return queryset.playable_by(actor, value).distinct()
|
||||||
|
|
||||||
def filter_has_albums(self, queryset, name, value):
|
def filter_has_albums(self, queryset, name, value):
|
||||||
return queryset.filter(albums__isnull=not value)
|
return queryset.filter(artist_credit__albums__isnull=not value)
|
||||||
|
|
||||||
def filter_has_mbid(self, queryset, name, value):
|
def filter_has_mbid(self, queryset, name, value):
|
||||||
return queryset.filter(mbid__isnull=(not value))
|
return queryset.filter(mbid__isnull=(not value))
|
||||||
|
@ -149,8 +149,16 @@ class TrackFilter(
|
||||||
moderation_filters.HiddenContentFilterSet,
|
moderation_filters.HiddenContentFilterSet,
|
||||||
):
|
):
|
||||||
q = fields.SearchFilter(
|
q = fields.SearchFilter(
|
||||||
search_fields=["title", "album__title", "artist__name"],
|
search_fields=[
|
||||||
fts_search_fields=["body_text", "artist__body_text", "album__body_text"],
|
"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")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
tag = TAG_FILTER
|
tag = TAG_FILTER
|
||||||
|
@ -173,8 +181,11 @@ class TrackFilter(
|
||||||
("size", "size"),
|
("size", "size"),
|
||||||
("position", "position"),
|
("position", "position"),
|
||||||
("disc_number", "disc_number"),
|
("disc_number", "disc_number"),
|
||||||
("artist__name", "artist__name"),
|
("artist_credit__artist__name", "artist_credit__artist__name"),
|
||||||
("artist__modification_date", "artist__modification_date"),
|
(
|
||||||
|
"artist_credit__artist__modification_date",
|
||||||
|
"artist_credit__artist__modification_date",
|
||||||
|
),
|
||||||
("?", "random"),
|
("?", "random"),
|
||||||
("tag_matches", "related"),
|
("tag_matches", "related"),
|
||||||
)
|
)
|
||||||
|
@ -205,16 +216,19 @@ class TrackFilter(
|
||||||
"mbid": ["exact"],
|
"mbid": ["exact"],
|
||||||
}
|
}
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["TRACK"]
|
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"
|
channel_filter_field = "track"
|
||||||
library_filter_field = "track"
|
library_filter_field = "track"
|
||||||
|
artist_credit_filter_field = "artist__credit__artist"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
return queryset.playable_by(actor, value).distinct()
|
return queryset.playable_by(actor, value).distinct()
|
||||||
|
|
||||||
def filter_artist(self, queryset, name, value):
|
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)
|
||||||
|
)
|
||||||
|
|
||||||
def filter_format(self, queryset, name, value):
|
def filter_format(self, queryset, name, value):
|
||||||
mimetypes = [utils.get_type_from_ext(e) for e in value.split(",")]
|
mimetypes = [utils.get_type_from_ext(e) for e in value.split(",")]
|
||||||
|
@ -238,8 +252,8 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||||
library = filters.CharFilter("library__uuid")
|
library = filters.CharFilter("library__uuid")
|
||||||
channel = filters.CharFilter("library__channel__uuid")
|
channel = filters.CharFilter("library__channel__uuid")
|
||||||
track = filters.UUIDFilter("track__uuid")
|
track = filters.UUIDFilter("track__uuid")
|
||||||
track_artist = filters.UUIDFilter("track__artist__uuid")
|
track_artist = filters.UUIDFilter("track__artist_credit__artist__uuid")
|
||||||
album_artist = filters.UUIDFilter("track__album__artist__uuid")
|
album_artist = filters.UUIDFilter("track__album__artist_credit__artist__uuid")
|
||||||
library = filters.UUIDFilter("library__uuid")
|
library = filters.UUIDFilter("library__uuid")
|
||||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
scope = common_filters.ActorScopeFilter(
|
scope = common_filters.ActorScopeFilter(
|
||||||
|
@ -273,7 +287,7 @@ class UploadFilter(audio_filters.IncludeChannelsFilterSet):
|
||||||
"mimetype",
|
"mimetype",
|
||||||
"import_reference",
|
"import_reference",
|
||||||
]
|
]
|
||||||
include_channels_field = "track__artist__channel"
|
include_channels_field = "track__artist_credit__artist__channel"
|
||||||
|
|
||||||
def filter_playable(self, queryset, name, value):
|
def filter_playable(self, queryset, name, value):
|
||||||
actor = utils.get_actor_from_request(self.request)
|
actor = utils.get_actor_from_request(self.request)
|
||||||
|
@ -289,10 +303,10 @@ class AlbumFilter(
|
||||||
):
|
):
|
||||||
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
playable = filters.BooleanFilter(field_name="_", method="filter_playable")
|
||||||
q = fields.SearchFilter(
|
q = fields.SearchFilter(
|
||||||
search_fields=["title", "artist__name"],
|
search_fields=["title", "artist_credit__artist__name"],
|
||||||
fts_search_fields=["body_text", "artist__body_text"],
|
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
|
tag = TAG_FILTER
|
||||||
scope = common_filters.ActorScopeFilter(
|
scope = common_filters.ActorScopeFilter(
|
||||||
actor_field="tracks__uploads__library__actor",
|
actor_field="tracks__uploads__library__actor",
|
||||||
|
@ -305,7 +319,10 @@ class AlbumFilter(
|
||||||
("creation_date", "creation_date"),
|
("creation_date", "creation_date"),
|
||||||
("release_date", "release_date"),
|
("release_date", "release_date"),
|
||||||
("title", "title"),
|
("title", "title"),
|
||||||
("artist__modification_date", "artist__modification_date"),
|
(
|
||||||
|
"artist_credit__artist__modification_date",
|
||||||
|
"artist_credit__artist__modification_date",
|
||||||
|
),
|
||||||
("?", "random"),
|
("?", "random"),
|
||||||
("tag_matches", "related"),
|
("tag_matches", "related"),
|
||||||
)
|
)
|
||||||
|
@ -326,15 +343,18 @@ class AlbumFilter(
|
||||||
)
|
)
|
||||||
|
|
||||||
has_release_date = filters.BooleanFilter(
|
has_release_date = filters.BooleanFilter(
|
||||||
field_name="_",
|
field_name="_", method="filter_has_release_date"
|
||||||
method="filter_has_release_date",
|
)
|
||||||
|
|
||||||
|
artist = filters.ModelChoiceFilter(
|
||||||
|
field_name="_", method="filter_artist", queryset=models.Artist.objects.all()
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Album
|
model = models.Album
|
||||||
fields = ["artist", "mbid"]
|
fields = ["artist_credit", "mbid"]
|
||||||
hidden_content_fields_mapping = moderation_filters.USER_FILTER_CONFIG["ALBUM"]
|
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"
|
channel_filter_field = "track__album"
|
||||||
library_filter_field = "track__album"
|
library_filter_field = "track__album"
|
||||||
|
|
||||||
|
@ -354,6 +374,9 @@ class AlbumFilter(
|
||||||
def filter_has_release_date(self, queryset, name, value):
|
def filter_has_release_date(self, queryset, name, value):
|
||||||
return queryset.filter(release_date__isnull=(not value))
|
return queryset.filter(release_date__isnull=(not value))
|
||||||
|
|
||||||
|
def filter_artist(self, queryset, name, value):
|
||||||
|
return queryset.filter(artist_credit__artist=value)
|
||||||
|
|
||||||
|
|
||||||
class LibraryFilter(filters.FilterSet):
|
class LibraryFilter(filters.FilterSet):
|
||||||
q = fields.SearchFilter(
|
q = fields.SearchFilter(
|
||||||
|
|
|
@ -12,11 +12,14 @@ class Importer:
|
||||||
|
|
||||||
def load(self, cleaned_data, raw_data, import_hooks):
|
def load(self, cleaned_data, raw_data, import_hooks):
|
||||||
mbid = cleaned_data.pop("mbid")
|
mbid = cleaned_data.pop("mbid")
|
||||||
|
artists_credits = cleaned_data.pop("artist_credit", None)
|
||||||
# let's validate data, just in case
|
# let's validate data, just in case
|
||||||
instance = self.model(**cleaned_data)
|
instance = self.model(**cleaned_data)
|
||||||
exclude = EXCLUDE_VALIDATION.get(self.model.__name__, [])
|
exclude = EXCLUDE_VALIDATION.get(self.model.__name__, [])
|
||||||
instance.full_clean(exclude=["mbid", "uuid", "fid", "from_activity"] + exclude)
|
instance.full_clean(exclude=["mbid", "uuid", "fid", "from_activity"] + exclude)
|
||||||
m = self.model.objects.update_or_create(mbid=mbid, defaults=cleaned_data)[0]
|
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:
|
for hook in import_hooks:
|
||||||
hook(m, cleaned_data, raw_data)
|
hook(m, cleaned_data, raw_data)
|
||||||
return m
|
return m
|
||||||
|
@ -47,4 +50,9 @@ class Mapping:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
registry = {"Artist": Importer, "Track": Importer, "Album": Importer}
|
registry = {
|
||||||
|
"Artist": Importer,
|
||||||
|
"ArtistCredit": Importer,
|
||||||
|
"Track": Importer,
|
||||||
|
"Album": Importer,
|
||||||
|
}
|
||||||
|
|
|
@ -700,10 +700,10 @@ def handle_modified(event, stdout, library, in_place, **kwargs):
|
||||||
to_update = (
|
to_update = (
|
||||||
existing_candidates.in_place()
|
existing_candidates.in_place()
|
||||||
.filter(source=source)
|
.filter(source=source)
|
||||||
.select_related(
|
.select_related("track__attributed_to")
|
||||||
"track__attributed_to",
|
.prefetch_related(
|
||||||
"track__artist",
|
"track__artist_credit__artist",
|
||||||
"track__album__artist",
|
"track__album__artist_credit__artist",
|
||||||
)
|
)
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
@ -839,9 +839,9 @@ def check_upload(stdout, upload):
|
||||||
" Cannot update track metadata, track belongs to someone else"
|
" Cannot update track metadata, track belongs to someone else"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
track = models.Track.objects.select_related("artist", "album__artist").get(
|
track = models.Track.objects.prefetch_related(
|
||||||
pk=upload.track_id
|
"artist_credit__artist", "album__artist_credit__artist"
|
||||||
)
|
).get(pk=upload.track_id)
|
||||||
try:
|
try:
|
||||||
tasks.update_track_metadata(upload.get_metadata(), track)
|
tasks.update_track_metadata(upload.get_metadata(), track)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
|
|
|
@ -492,61 +492,76 @@ class ArtistField(serializers.Field):
|
||||||
|
|
||||||
return final
|
return final
|
||||||
|
|
||||||
def to_internal_value(self, data):
|
def _get_artist_credit_tuple(self, mbids, data):
|
||||||
# we have multiple values that can be separated by various separators
|
from . import tasks
|
||||||
separators = [";", ","]
|
|
||||||
|
names_artists_credits_tuples = tasks.parse_credits(
|
||||||
|
data.get("names", ""), None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
artist_artists_credits_tuples = tasks.parse_credits(
|
||||||
|
data.get("artists", ""), None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
len_mbids = len(mbids)
|
||||||
|
if (
|
||||||
|
len(names_artists_credits_tuples) != len_mbids
|
||||||
|
and len(artist_artists_credits_tuples) != len_mbids
|
||||||
|
):
|
||||||
|
logger.warning(
|
||||||
|
"Error parsing artist data, not the same amount of mbids and parsed artists. \
|
||||||
|
Probably because the artist parser found more artists than there is."
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(names_artists_credits_tuples) > len(artist_artists_credits_tuples):
|
||||||
|
return names_artists_credits_tuples
|
||||||
|
return artist_artists_credits_tuples
|
||||||
|
|
||||||
|
def _get_mbids(self, raw_mbids):
|
||||||
|
# we have multiple mbid values that can be separated by various separators
|
||||||
|
separators = [";", ",", "/"]
|
||||||
# we get a list like that if tagged via musicbrainz
|
# we get a list like that if tagged via musicbrainz
|
||||||
# ae29aae4-abfb-4609-8f54-417b1f4d64cc; 3237b5a8-ae44-400c-aa6d-cea51f0b9074;
|
# ae29aae4-abfb-4609-8f54-417b1f4d64cc; 3237b5a8-ae44-400c-aa6d-cea51f0b9074;
|
||||||
raw_mbids = data["mbids"]
|
|
||||||
used_separator = None
|
|
||||||
mbids = [raw_mbids]
|
mbids = [raw_mbids]
|
||||||
if 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):
|
|
||||||
for separator in separators:
|
for separator in separators:
|
||||||
if separator in data["artists"]:
|
if separator in raw_mbids:
|
||||||
names = [n.strip() for n in data["artists"].split(separator)]
|
mbids = [m.strip() for m in raw_mbids.split(separator)]
|
||||||
break
|
break
|
||||||
# corner case: 'album artist' field with only one artist but multiple names in 'artits' field
|
return mbids
|
||||||
if (
|
|
||||||
not names
|
def _format_artist_credit_list(self, artists_credits_tuples, mbids):
|
||||||
and data.get("names", None)
|
final_artist_credits = []
|
||||||
and any(separator in data["names"] for separator in separators)
|
for i, ac in enumerate(artists_credits_tuples):
|
||||||
):
|
artist_credit = {
|
||||||
names = [n.strip() for n in data["names"].split(separators[0])]
|
"credit": ac[0],
|
||||||
elif not names:
|
"mbid": (mbids[i] if 0 <= i < len(mbids) else None),
|
||||||
names = [data["artists"]]
|
"joinphrase": ac[1],
|
||||||
elif used_separator and mbids:
|
"index": i,
|
||||||
names = [n.strip() for n in data["names"].split(used_separator)]
|
}
|
||||||
else:
|
final_artist_credits.append(artist_credit)
|
||||||
names = [data["names"]]
|
|
||||||
|
return final_artist_credits
|
||||||
|
|
||||||
|
def to_internal_value(self, data):
|
||||||
|
if (
|
||||||
|
self.context.get("strict", True)
|
||||||
|
and not data.get("artists", [])
|
||||||
|
and not data.get("names", [])
|
||||||
|
):
|
||||||
|
raise serializers.ValidationError("This field is required.")
|
||||||
|
mbids = self._get_mbids(data["mbids"])
|
||||||
|
# now, we split on artist names
|
||||||
|
artists_credits_tuples = self._get_artist_credit_tuple(mbids, data)
|
||||||
|
final_artist_credits = self._format_artist_credit_list(
|
||||||
|
artists_credits_tuples, mbids
|
||||||
|
)
|
||||||
|
|
||||||
final = []
|
|
||||||
for i, name in enumerate(names):
|
|
||||||
try:
|
|
||||||
mbid = mbids[i]
|
|
||||||
except IndexError:
|
|
||||||
mbid = None
|
|
||||||
artist = {"name": name, "mbid": mbid}
|
|
||||||
final.append(artist)
|
|
||||||
field = serializers.ListField(
|
field = serializers.ListField(
|
||||||
child=ArtistSerializer(strict=self.context.get("strict", True)),
|
child=ArtistSerializer(strict=self.context.get("strict", True)),
|
||||||
min_length=1,
|
min_length=1,
|
||||||
)
|
)
|
||||||
return field.to_internal_value(final)
|
return field.to_internal_value(final_artist_credits)
|
||||||
|
|
||||||
|
|
||||||
class AlbumField(serializers.Field):
|
class AlbumField(serializers.Field):
|
||||||
|
@ -565,16 +580,17 @@ class AlbumField(serializers.Field):
|
||||||
"release_date": data.get("date", None),
|
"release_date": data.get("date", None),
|
||||||
"mbid": data.get("musicbrainz_albumid", None),
|
"mbid": data.get("musicbrainz_albumid", None),
|
||||||
}
|
}
|
||||||
artists_field = ArtistField(for_album=True)
|
artist_credit_field = ArtistField(for_album=True)
|
||||||
payload = artists_field.get_value(data)
|
payload = artist_credit_field.get_value(data)
|
||||||
try:
|
try:
|
||||||
artists = artists_field.to_internal_value(payload)
|
artist_credit = artist_credit_field.to_internal_value(payload)
|
||||||
except serializers.ValidationError as e:
|
except serializers.ValidationError as e:
|
||||||
artists = []
|
artist_credit = []
|
||||||
logger.debug("Ignoring validation error on album artists: %s", e)
|
logger.debug("Ignoring validation error on album artist_credit: %s", e)
|
||||||
album_serializer = AlbumSerializer(data=final)
|
album_serializer = AlbumSerializer(data=final)
|
||||||
album_serializer.is_valid(raise_exception=True)
|
album_serializer.is_valid(raise_exception=True)
|
||||||
album_serializer.validated_data["artists"] = artists
|
album_serializer.validated_data["artist_credit"] = artist_credit
|
||||||
|
|
||||||
return album_serializer.validated_data
|
return album_serializer.validated_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -655,14 +671,17 @@ class MBIDField(serializers.UUIDField):
|
||||||
|
|
||||||
|
|
||||||
class ArtistSerializer(serializers.Serializer):
|
class ArtistSerializer(serializers.Serializer):
|
||||||
name = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
credit = serializers.CharField(required=False, allow_null=True, allow_blank=True)
|
||||||
mbid = MBIDField()
|
mbid = MBIDField()
|
||||||
|
joinphrase = serializers.CharField(
|
||||||
|
trim_whitespace=False, required=False, allow_null=True, allow_blank=True
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.strict = kwargs.pop("strict", True)
|
self.strict = kwargs.pop("strict", True)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def validate_name(self, v):
|
def validate_credit(self, v):
|
||||||
if self.strict and not v:
|
if self.strict and not v:
|
||||||
raise serializers.ValidationError("This field is required.")
|
raise serializers.ValidationError("This field is required.")
|
||||||
return v
|
return v
|
||||||
|
@ -731,7 +750,7 @@ class TrackMetadataSerializer(serializers.Serializer):
|
||||||
description = DescriptionField(allow_null=True, allow_blank=True, required=False)
|
description = DescriptionField(allow_null=True, allow_blank=True, required=False)
|
||||||
|
|
||||||
album = AlbumField()
|
album = AlbumField()
|
||||||
artists = ArtistField()
|
artist_credit = ArtistField()
|
||||||
cover_data = CoverDataField(required=False)
|
cover_data = CoverDataField(required=False)
|
||||||
|
|
||||||
remove_blank_null_fields = [
|
remove_blank_null_fields = [
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
# 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 save_artist_credit(obj, ArtistCredit):
|
||||||
|
artist_credit, created = ArtistCredit.objects.get_or_create(
|
||||||
|
artist=obj.artist,
|
||||||
|
joinphrase="",
|
||||||
|
credit=obj.artist.name,
|
||||||
|
)
|
||||||
|
obj.artist_credit.set([artist_credit])
|
||||||
|
obj.save()
|
||||||
|
|
||||||
|
|
||||||
|
def set_all_artists_credit(apps, schema_editor):
|
||||||
|
Track = apps.get_model("music", "Track")
|
||||||
|
Album = apps.get_model("music", "Album")
|
||||||
|
ArtistCredit = apps.get_model("music", "ArtistCredit")
|
||||||
|
|
||||||
|
for track in Track.objects.all():
|
||||||
|
save_artist_credit(track, ArtistCredit)
|
||||||
|
|
||||||
|
for album in Album.objects.all():
|
||||||
|
save_artist_credit(album, ArtistCredit)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("music", "0058_upload_quality"),
|
||||||
|
]
|
||||||
|
|
||||||
|
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_all_artists_credit, skip),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="album",
|
||||||
|
name="artist",
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="track",
|
||||||
|
name="artist",
|
||||||
|
),
|
||||||
|
]
|
|
@ -27,7 +27,7 @@ from django.utils import timezone
|
||||||
from funkwhale_api import musicbrainz
|
from funkwhale_api import musicbrainz
|
||||||
from funkwhale_api.common import fields
|
from funkwhale_api.common import fields
|
||||||
from funkwhale_api.common import models as common_models
|
from funkwhale_api.common import models as common_models
|
||||||
from funkwhale_api.common import session
|
from funkwhale_api.common import preferences, session
|
||||||
from funkwhale_api.common import utils as common_utils
|
from funkwhale_api.common import utils as common_utils
|
||||||
from funkwhale_api.federation import models as federation_models
|
from funkwhale_api.federation import models as federation_models
|
||||||
from funkwhale_api.federation import utils as federation_utils
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
|
@ -112,7 +112,6 @@ class APIModelMixin(models.Model):
|
||||||
def get_federation_id(self):
|
def get_federation_id(self):
|
||||||
if self.fid:
|
if self.fid:
|
||||||
return self.fid
|
return self.fid
|
||||||
|
|
||||||
return federation_utils.full_url(
|
return federation_utils.full_url(
|
||||||
reverse(
|
reverse(
|
||||||
f"federation:music:{self.federation_namespace}-detail",
|
f"federation:music:{self.federation_namespace}-detail",
|
||||||
|
@ -171,12 +170,12 @@ class License(models.Model):
|
||||||
|
|
||||||
class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
def with_albums_count(self):
|
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):
|
def with_albums(self):
|
||||||
return self.prefetch_related(
|
return self.prefetch_related(
|
||||||
models.Prefetch(
|
models.Prefetch(
|
||||||
"albums",
|
"artist_credit__albums",
|
||||||
queryset=Album.objects.with_tracks_count().select_related(
|
queryset=Album.objects.with_tracks_count().select_related(
|
||||||
"attachment_cover", "attributed_to"
|
"attachment_cover", "attributed_to"
|
||||||
),
|
),
|
||||||
|
@ -186,7 +185,7 @@ class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
def annotate_playable_by_actor(self, actor):
|
def annotate_playable_by_actor(self, actor):
|
||||||
tracks = (
|
tracks = (
|
||||||
Upload.objects.playable_by(actor)
|
Upload.objects.playable_by(actor)
|
||||||
.filter(track__artist=models.OuterRef("id"))
|
.filter(track__artist_credit__artist=models.OuterRef("id"))
|
||||||
.order_by("id")
|
.order_by("id")
|
||||||
.values("id")[:1]
|
.values("id")[:1]
|
||||||
)
|
)
|
||||||
|
@ -195,7 +194,9 @@ class ArtistQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
|
|
||||||
def playable_by(self, actor, include=True):
|
def playable_by(self, actor, include=True):
|
||||||
tracks = Track.objects.playable_by(actor)
|
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:
|
if include:
|
||||||
return self.filter(pk__in=matches)
|
return self.filter(pk__in=matches)
|
||||||
else:
|
else:
|
||||||
|
@ -272,9 +273,25 @@ class Artist(APIModelMixin):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def import_artist(v):
|
def import_artist_credit(v):
|
||||||
a = Artist.get_or_create_from_api(mbid=v[0]["artist"]["id"])[0]
|
artists_credits = []
|
||||||
return a
|
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):
|
def parse_date(v):
|
||||||
|
@ -290,6 +307,39 @@ def import_tracks(instance, cleaned_data, raw_data):
|
||||||
importers.load(Track, track_cleaned_data, track_data, Track.import_hooks)
|
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):
|
class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
def with_tracks_count(self):
|
def with_tracks_count(self):
|
||||||
return self.annotate(_tracks_count=models.Count("tracks"))
|
return self.annotate(_tracks_count=models.Count("tracks"))
|
||||||
|
@ -297,7 +347,7 @@ class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
def annotate_playable_by_actor(self, actor):
|
def annotate_playable_by_actor(self, actor):
|
||||||
tracks = (
|
tracks = (
|
||||||
Upload.objects.playable_by(actor)
|
Upload.objects.playable_by(actor)
|
||||||
.filter(track__album=models.OuterRef("id"))
|
.filter(track__artist_credit__albums=models.OuterRef("id"))
|
||||||
.order_by("id")
|
.order_by("id")
|
||||||
.values("id")[:1]
|
.values("id")[:1]
|
||||||
)
|
)
|
||||||
|
@ -329,7 +379,7 @@ class AlbumQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
|
|
||||||
class Album(APIModelMixin):
|
class Album(APIModelMixin):
|
||||||
title = models.TextField()
|
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_date = models.DateField(null=True, blank=True, db_index=True)
|
||||||
release_group_id = models.UUIDField(null=True, blank=True)
|
release_group_id = models.UUIDField(null=True, blank=True)
|
||||||
attachment_cover = models.ForeignKey(
|
attachment_cover = models.ForeignKey(
|
||||||
|
@ -380,9 +430,9 @@ class Album(APIModelMixin):
|
||||||
"title": {"musicbrainz_field_name": "title"},
|
"title": {"musicbrainz_field_name": "title"},
|
||||||
"release_date": {"musicbrainz_field_name": "date", "converter": parse_date},
|
"release_date": {"musicbrainz_field_name": "date", "converter": parse_date},
|
||||||
"type": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()},
|
"type": {"musicbrainz_field_name": "type", "converter": lambda v: v.lower()},
|
||||||
"artist": {
|
"artist_credit": {
|
||||||
"musicbrainz_field_name": "artist-credit",
|
"musicbrainz_field_name": "artist-credit",
|
||||||
"converter": import_artist,
|
"converter": import_artist_credit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
objects = AlbumQuerySet.as_manager()
|
objects = AlbumQuerySet.as_manager()
|
||||||
|
@ -405,6 +455,13 @@ class Album(APIModelMixin):
|
||||||
kwargs.update({"title": title})
|
kwargs.update({"title": title})
|
||||||
return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
|
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):
|
def import_tags(instance, cleaned_data, raw_data):
|
||||||
MINIMUM_COUNT = 2
|
MINIMUM_COUNT = 2
|
||||||
|
@ -428,11 +485,11 @@ def import_album(v):
|
||||||
class TrackQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
class TrackQuerySet(common_models.LocalFromFidQuerySet, models.QuerySet):
|
||||||
def for_nested_serialization(self):
|
def for_nested_serialization(self):
|
||||||
return self.prefetch_related(
|
return self.prefetch_related(
|
||||||
"artist",
|
"artist_credit",
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"album",
|
"album",
|
||||||
queryset=Album.objects.select_related(
|
queryset=Album.objects.prefetch_related(
|
||||||
"artist", "attachment_cover"
|
"artist_credit", "attachment_cover"
|
||||||
).annotate(_prefetched_tracks_count=Count("tracks")),
|
).annotate(_prefetched_tracks_count=Count("tracks")),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -485,7 +542,7 @@ def get_artist(release_list):
|
||||||
class Track(APIModelMixin):
|
class Track(APIModelMixin):
|
||||||
mbid = models.UUIDField(db_index=True, null=True, blank=True)
|
mbid = models.UUIDField(db_index=True, null=True, blank=True)
|
||||||
title = models.TextField()
|
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)
|
disc_number = models.PositiveIntegerField(null=True, blank=True)
|
||||||
position = models.PositiveIntegerField(null=True, blank=True)
|
position = models.PositiveIntegerField(null=True, blank=True)
|
||||||
album = models.ForeignKey(
|
album = models.ForeignKey(
|
||||||
|
@ -527,11 +584,9 @@ class Track(APIModelMixin):
|
||||||
musicbrainz_mapping = {
|
musicbrainz_mapping = {
|
||||||
"mbid": {"musicbrainz_field_name": "id"},
|
"mbid": {"musicbrainz_field_name": "id"},
|
||||||
"title": {"musicbrainz_field_name": "title"},
|
"title": {"musicbrainz_field_name": "title"},
|
||||||
"artist": {
|
"artist_credit": {
|
||||||
"musicbrainz_field_name": "artist-credit",
|
"musicbrainz_field_name": "artist-credit",
|
||||||
"converter": lambda v: Artist.get_or_create_from_api(
|
"converter": import_artist_credit,
|
||||||
mbid=v[0]["artist"]["id"]
|
|
||||||
)[0],
|
|
||||||
},
|
},
|
||||||
"album": {"musicbrainz_field_name": "release-list", "converter": import_album},
|
"album": {"musicbrainz_field_name": "release-list", "converter": import_album},
|
||||||
}
|
}
|
||||||
|
@ -559,19 +614,21 @@ class Track(APIModelMixin):
|
||||||
def get_moderation_url(self):
|
def get_moderation_url(self):
|
||||||
return f"/manage/library/tracks/{self.pk}"
|
return f"/manage/library/tracks/{self.pk}"
|
||||||
|
|
||||||
def save(self, **kwargs):
|
@property
|
||||||
try:
|
def get_artist_credit_string(self):
|
||||||
self.artist
|
return utils.get_artist_credit_string(self)
|
||||||
except Artist.DoesNotExist:
|
|
||||||
self.artist = self.album.artist
|
def get_artists_list(self):
|
||||||
super().save(**kwargs)
|
return [ac.artist for ac in self.artist_credit.all()]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_name(self):
|
def full_name(self):
|
||||||
try:
|
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:
|
except AttributeError:
|
||||||
return f"{self.artist.name} - {self.title}"
|
return f"{self.get_artist_credit_string} - {self.title}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cover(self):
|
def cover(self):
|
||||||
|
@ -587,6 +644,9 @@ class Track(APIModelMixin):
|
||||||
kwargs.update({"title": title})
|
kwargs.update({"title": title})
|
||||||
return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
|
return cls.objects.get_or_create(title__iexact=title, defaults=kwargs)
|
||||||
|
|
||||||
|
# not used anymore, allow increase of performance when importing tracks using mbids.
|
||||||
|
# In its actual state it will not work since it assume track_data["recording"]["artist-credit"] can
|
||||||
|
# contain a joinphrase but it's not he case. Needs to be updated.
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_or_create_from_release(cls, release_mbid, mbid):
|
def get_or_create_from_release(cls, release_mbid, mbid):
|
||||||
release_mbid = str(release_mbid)
|
release_mbid = str(release_mbid)
|
||||||
|
@ -609,33 +669,42 @@ class Track(APIModelMixin):
|
||||||
if not track_data:
|
if not track_data:
|
||||||
raise ValueError("No track found matching this ID")
|
raise ValueError("No track found matching this ID")
|
||||||
|
|
||||||
track_artist_mbid = None
|
artists_credits = []
|
||||||
for ac in track_data["recording"]["artist-credit"]:
|
for i, ac in enumerate(track_data["recording"]["artist-credit"]):
|
||||||
try:
|
try:
|
||||||
ac_mbid = ac["artist"]["id"]
|
ac_mbid = ac["artist"]["id"]
|
||||||
except TypeError:
|
except TypeError:
|
||||||
# it's probably a string, like "feat."
|
# it's probably a string, like "feat.".
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if ac_mbid == str(album.artist.mbid):
|
track_artist = Artist.get_or_create_from_api(ac_mbid)[0]
|
||||||
continue
|
|
||||||
|
|
||||||
track_artist_mbid = ac_mbid
|
if "joinphrase" not in ac:
|
||||||
break
|
joinphrase = ""
|
||||||
track_artist_mbid = track_artist_mbid or album.artist.mbid
|
else:
|
||||||
if track_artist_mbid == str(album.artist.mbid):
|
joinphrase = ac["joinphrase"]
|
||||||
track_artist = album.artist
|
|
||||||
else:
|
artist_credit, create = ArtistCredit.objects.get_or_create(
|
||||||
track_artist = Artist.get_or_create_from_api(track_artist_mbid)[0]
|
artist=track_artist,
|
||||||
return cls.objects.update_or_create(
|
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,
|
mbid=mbid,
|
||||||
defaults={
|
defaults={
|
||||||
"position": int(track["position"]),
|
"position": int(track["position"]),
|
||||||
"title": track["recording"]["title"],
|
"title": track["recording"]["title"],
|
||||||
"album": album,
|
"album": album,
|
||||||
"artist": track_artist,
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
track[0].artist_credit.set(artists_credits)
|
||||||
|
return track
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def listen_url(self) -> str:
|
def listen_url(self) -> str:
|
||||||
|
@ -809,7 +878,7 @@ class Upload(models.Model):
|
||||||
title_parts.append(self.track.title)
|
title_parts.append(self.track.title)
|
||||||
if self.track.album:
|
if self.track.album:
|
||||||
title_parts.append(self.track.album.title)
|
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)
|
title = " - ".join(title_parts)
|
||||||
filename = f"{title}.{extension}"
|
filename = f"{title}.{extension}"
|
||||||
|
@ -1032,9 +1101,21 @@ class Upload(models.Model):
|
||||||
if self.track.album
|
if self.track.album
|
||||||
else tags_models.TaggedItem.objects.none()
|
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
|
return items
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -126,7 +126,11 @@ class TrackMutationSerializer(CoverMutation, TagMutation, DescriptionMutation):
|
||||||
return serialized_relations
|
return serialized_relations
|
||||||
|
|
||||||
def post_apply(self, obj, validated_data):
|
def post_apply(self, obj, validated_data):
|
||||||
channel = obj.artist.get_channel()
|
channel = (
|
||||||
|
obj.artist_credit.all()[0].artist.get_channel()
|
||||||
|
if len(obj.artist_credit.all()) == 1
|
||||||
|
else None
|
||||||
|
)
|
||||||
if channel:
|
if channel:
|
||||||
upload = channel.library.uploads.filter(track=obj).first()
|
upload = channel.library.uploads.filter(track=obj).first()
|
||||||
if upload:
|
if upload:
|
||||||
|
|
|
@ -81,12 +81,12 @@ class ArtistAlbumSerializer(serializers.Serializer):
|
||||||
fid = serializers.URLField()
|
fid = serializers.URLField()
|
||||||
mbid = serializers.UUIDField()
|
mbid = serializers.UUIDField()
|
||||||
title = serializers.CharField()
|
title = serializers.CharField()
|
||||||
artist = serializers.SerializerMethodField()
|
artist_credit = serializers.SerializerMethodField()
|
||||||
release_date = serializers.DateField()
|
release_date = serializers.DateField()
|
||||||
creation_date = serializers.DateTimeField()
|
creation_date = serializers.DateTimeField()
|
||||||
|
|
||||||
def get_artist(self, o) -> int:
|
def get_artist_credit(self, o) -> int:
|
||||||
return o.artist_id
|
return [ac.id for ac in o.artist_credit.all()]
|
||||||
|
|
||||||
def get_tracks_count(self, o) -> int:
|
def get_tracks_count(self, o) -> int:
|
||||||
return len(o.tracks.all())
|
return len(o.tracks.all())
|
||||||
|
@ -113,7 +113,7 @@ class ArtistWithAlbumsInlineChannelSerializer(serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||||
albums = ArtistAlbumSerializer(many=True)
|
albums = serializers.SerializerMethodField()
|
||||||
tags = serializers.SerializerMethodField()
|
tags = serializers.SerializerMethodField()
|
||||||
attributed_to = APIActorSerializer(allow_null=True)
|
attributed_to = APIActorSerializer(allow_null=True)
|
||||||
channel = ArtistWithAlbumsInlineChannelSerializer(allow_null=True)
|
channel = ArtistWithAlbumsInlineChannelSerializer(allow_null=True)
|
||||||
|
@ -127,14 +127,17 @@ class ArtistWithAlbumsSerializer(OptionalDescriptionMixin, serializers.Serialize
|
||||||
is_local = serializers.BooleanField()
|
is_local = serializers.BooleanField()
|
||||||
cover = CoverField(allow_null=True)
|
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"}})
|
@extend_schema_field({"type": "array", "items": {"type": "string"}})
|
||||||
def get_tags(self, obj):
|
def get_tags(self, obj):
|
||||||
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
tagged_items = getattr(obj, "_prefetched_tagged_items", [])
|
||||||
return [ti.tag.name for ti in tagged_items]
|
return [ti.tag.name for ti in tagged_items]
|
||||||
|
|
||||||
def get_tracks_count(self, o) -> int:
|
def get_tracks_count(self, o) -> int:
|
||||||
tracks = getattr(o, "_prefetched_tracks", None)
|
return getattr(o, "_tracks_count", 0)
|
||||||
return len(tracks) if tracks else 0
|
|
||||||
|
|
||||||
|
|
||||||
class SimpleArtistSerializer(serializers.ModelSerializer):
|
class SimpleArtistSerializer(serializers.ModelSerializer):
|
||||||
|
@ -156,11 +159,20 @@ class SimpleArtistSerializer(serializers.ModelSerializer):
|
||||||
"description",
|
"description",
|
||||||
"attachment_cover",
|
"attachment_cover",
|
||||||
"channel",
|
"channel",
|
||||||
|
"attributed_to",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
class ArtistCreditSerializer(serializers.ModelSerializer):
|
||||||
artist = SimpleArtistSerializer()
|
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)
|
cover = CoverField(allow_null=True)
|
||||||
is_playable = serializers.SerializerMethodField()
|
is_playable = serializers.SerializerMethodField()
|
||||||
tags = serializers.SerializerMethodField()
|
tags = serializers.SerializerMethodField()
|
||||||
|
@ -203,7 +215,7 @@ class AlbumSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||||
|
|
||||||
|
|
||||||
class TrackAlbumSerializer(serializers.ModelSerializer):
|
class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
artist = SimpleArtistSerializer()
|
artist_credit = ArtistCreditSerializer(many=True)
|
||||||
cover = CoverField(allow_null=True)
|
cover = CoverField(allow_null=True)
|
||||||
tracks_count = serializers.SerializerMethodField()
|
tracks_count = serializers.SerializerMethodField()
|
||||||
|
|
||||||
|
@ -217,7 +229,7 @@ class TrackAlbumSerializer(serializers.ModelSerializer):
|
||||||
"fid",
|
"fid",
|
||||||
"mbid",
|
"mbid",
|
||||||
"title",
|
"title",
|
||||||
"artist",
|
"artist_credit",
|
||||||
"release_date",
|
"release_date",
|
||||||
"cover",
|
"cover",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
|
@ -257,7 +269,7 @@ def sort_uploads_for_listen(uploads):
|
||||||
|
|
||||||
|
|
||||||
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
class TrackSerializer(OptionalDescriptionMixin, serializers.Serializer):
|
||||||
artist = SimpleArtistSerializer()
|
artist_credit = ArtistCreditSerializer(many=True)
|
||||||
album = TrackAlbumSerializer(read_only=True)
|
album = TrackAlbumSerializer(read_only=True)
|
||||||
uploads = serializers.SerializerMethodField()
|
uploads = serializers.SerializerMethodField()
|
||||||
listen_url = serializers.SerializerMethodField()
|
listen_url = serializers.SerializerMethodField()
|
||||||
|
@ -400,9 +412,9 @@ class UploadSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
def filter_album(qs, context):
|
def filter_album(qs, context):
|
||||||
if "channel" in 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:
|
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()
|
return qs.none()
|
||||||
|
|
||||||
|
@ -567,12 +579,12 @@ class SimpleAlbumSerializer(serializers.ModelSerializer):
|
||||||
class TrackActivitySerializer(activity_serializers.ModelSerializer):
|
class TrackActivitySerializer(activity_serializers.ModelSerializer):
|
||||||
type = serializers.SerializerMethodField()
|
type = serializers.SerializerMethodField()
|
||||||
name = serializers.CharField(source="title")
|
name = serializers.CharField(source="title")
|
||||||
artist = serializers.CharField(source="artist.name")
|
artist_credit = serializers.CharField(source="get_artist_credit_string")
|
||||||
album = serializers.SerializerMethodField()
|
album = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.Track
|
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):
|
def get_type(self, obj):
|
||||||
return "Audio"
|
return "Audio"
|
||||||
|
@ -612,9 +624,9 @@ class OembedSerializer(serializers.Serializer):
|
||||||
embed_id = None
|
embed_id = None
|
||||||
embed_type = None
|
embed_type = None
|
||||||
if match.url_name == "library_track":
|
if match.url_name == "library_track":
|
||||||
qs = models.Track.objects.select_related("artist", "album__artist").filter(
|
qs = models.Track.objects.prefetch_related(
|
||||||
pk=int(match.kwargs["pk"])
|
"artist_credit", "album__artist_credit"
|
||||||
)
|
).filter(pk=int(match.kwargs["pk"]))
|
||||||
try:
|
try:
|
||||||
track = qs.get()
|
track = qs.get()
|
||||||
except models.Track.DoesNotExist:
|
except models.Track.DoesNotExist:
|
||||||
|
@ -623,7 +635,7 @@ class OembedSerializer(serializers.Serializer):
|
||||||
)
|
)
|
||||||
embed_type = "track"
|
embed_type = "track"
|
||||||
embed_id = track.pk
|
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:
|
if track.attachment_cover:
|
||||||
data[
|
data[
|
||||||
"thumbnail_url"
|
"thumbnail_url"
|
||||||
|
@ -637,15 +649,17 @@ class OembedSerializer(serializers.Serializer):
|
||||||
data["thumbnail_width"] = 200
|
data["thumbnail_width"] = 200
|
||||||
data["thumbnail_height"] = 200
|
data["thumbnail_height"] = 200
|
||||||
data["description"] = track.full_name
|
data["description"] = track.full_name
|
||||||
data["author_name"] = track.artist.name
|
data["author_name"] = track.get_artist_credit_string
|
||||||
data["height"] = 150
|
data["height"] = 150
|
||||||
|
# here we take the first artist since oembed standard do not allow a list of url
|
||||||
data["author_url"] = federation_utils.full_url(
|
data["author_url"] = federation_utils.full_url(
|
||||||
common_utils.spa_reverse(
|
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":
|
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"])
|
pk=int(match.kwargs["pk"])
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
@ -662,15 +676,17 @@ class OembedSerializer(serializers.Serializer):
|
||||||
] = album.attachment_cover.download_url_medium_square_crop
|
] = album.attachment_cover.download_url_medium_square_crop
|
||||||
data["thumbnail_width"] = 200
|
data["thumbnail_width"] = 200
|
||||||
data["thumbnail_height"] = 200
|
data["thumbnail_height"] = 200
|
||||||
data["title"] = f"{album.title} by {album.artist.name}"
|
data["title"] = f"{album.title} by {album.get_artist_credit_string}"
|
||||||
data["description"] = f"{album.title} by {album.artist.name}"
|
data["description"] = f"{album.title} by {album.get_artist_credit_string}"
|
||||||
data["author_name"] = album.artist.name
|
data["author_name"] = album.get_artist_credit_string
|
||||||
data["height"] = 400
|
data["height"] = 400
|
||||||
data["author_url"] = federation_utils.full_url(
|
data["author_url"] = federation_utils.full_url(
|
||||||
common_utils.spa_reverse(
|
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":
|
elif match.url_name == "library_artist":
|
||||||
qs = models.Artist.objects.filter(pk=int(match.kwargs["pk"]))
|
qs = models.Artist.objects.filter(pk=int(match.kwargs["pk"]))
|
||||||
try:
|
try:
|
||||||
|
@ -681,7 +697,17 @@ class OembedSerializer(serializers.Serializer):
|
||||||
)
|
)
|
||||||
embed_type = "artist"
|
embed_type = "artist"
|
||||||
embed_id = artist.pk
|
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:
|
if album and album.attachment_cover:
|
||||||
data[
|
data[
|
||||||
|
@ -791,32 +817,34 @@ class AlbumCreateSerializer(serializers.Serializer):
|
||||||
release_date = serializers.DateField(required=False, allow_null=True)
|
release_date = serializers.DateField(required=False, allow_null=True)
|
||||||
tags = tags_serializers.TagsListField(required=False)
|
tags = tags_serializers.TagsListField(required=False)
|
||||||
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
description = common_serializers.ContentSerializer(allow_null=True, required=False)
|
||||||
|
# only used in album channel creation, so this is not a list
|
||||||
artist = common_serializers.RelatedField(
|
artist_credit = common_serializers.RelatedField(
|
||||||
"id",
|
"id",
|
||||||
queryset=models.Artist.objects.exclude(channel__isnull=True),
|
queryset=models.ArtistCredit.objects.exclude(artist__channel__isnull=True),
|
||||||
required=True,
|
required=True,
|
||||||
serializer=None,
|
serializer=None,
|
||||||
filters=lambda context: {"attributed_to": context["user"].actor},
|
many=True,
|
||||||
|
filters=lambda context: {"artist__attributed_to": context["user"].actor},
|
||||||
)
|
)
|
||||||
|
|
||||||
def validate(self, validated_data):
|
def validate(self, validated_data):
|
||||||
duplicates = validated_data["artist"].albums.filter(
|
duplicates = models.Album.objects.none()
|
||||||
title__iexact=validated_data["title"]
|
for ac in validated_data["artist_credit"]:
|
||||||
)
|
duplicates = duplicates | ac.albums.filter(
|
||||||
|
title__iexact=validated_data["title"]
|
||||||
|
)
|
||||||
if duplicates.exists():
|
if duplicates.exists():
|
||||||
raise serializers.ValidationError("An album with this title already exist")
|
raise serializers.ValidationError("An album with this title already exist")
|
||||||
|
|
||||||
return super().validate(validated_data)
|
return super().validate(validated_data)
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
obj.artist.attachment_cover
|
|
||||||
return AlbumSerializer(obj, context=self.context).data
|
return AlbumSerializer(obj, context=self.context).data
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
instance = models.Album.objects.create(
|
instance = models.Album.objects.create(
|
||||||
attributed_to=self.context["user"].actor,
|
attributed_to=self.context["user"].actor,
|
||||||
artist=validated_data["artist"],
|
|
||||||
release_date=validated_data.get("release_date"),
|
release_date=validated_data.get("release_date"),
|
||||||
title=validated_data["title"],
|
title=validated_data["title"],
|
||||||
attachment_cover=validated_data.get("cover"),
|
attachment_cover=validated_data.get("cover"),
|
||||||
|
@ -825,7 +853,8 @@ class AlbumCreateSerializer(serializers.Serializer):
|
||||||
instance, "description", validated_data.get("description")
|
instance, "description", validated_data.get("description")
|
||||||
)
|
)
|
||||||
tag_models.set_tags(instance, *(validated_data.get("tags", []) or []))
|
tag_models.set_tags(instance, *(validated_data.get("tags", []) or []))
|
||||||
instance.artist.get_channel()
|
|
||||||
|
instance.artist_credit.set(validated_data["artist_credit"])
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,9 @@ def get_twitter_card_metas(type, id):
|
||||||
|
|
||||||
|
|
||||||
def library_track(request, pk, redirect_to_ap):
|
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:
|
try:
|
||||||
obj = queryset.get()
|
obj = queryset.get()
|
||||||
except models.Track.DoesNotExist:
|
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": "og:type", "content": "music.song"},
|
||||||
{"tag": "meta", "property": "music:album:disc", "content": obj.disc_number},
|
{"tag": "meta", "property": "music:album:disc", "content": obj.disc_number},
|
||||||
{"tag": "meta", "property": "music:album:track", "content": obj.position},
|
{"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:
|
if obj.album:
|
||||||
metas.append(
|
metas.append(
|
||||||
|
@ -119,7 +125,7 @@ def library_track(request, pk, redirect_to_ap):
|
||||||
|
|
||||||
|
|
||||||
def library_album(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:
|
try:
|
||||||
obj = queryset.get()
|
obj = queryset.get()
|
||||||
except models.Album.DoesNotExist:
|
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:url", "content": album_url},
|
||||||
{"tag": "meta", "property": "og:title", "content": obj.title},
|
{"tag": "meta", "property": "og:title", "content": obj.title},
|
||||||
{"tag": "meta", "property": "og:type", "content": "music.album"},
|
{"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:
|
if obj.release_date:
|
||||||
metas.append(
|
metas.append(
|
||||||
{
|
{
|
||||||
|
@ -206,7 +216,10 @@ def library_artist(request, pk, redirect_to_ap):
|
||||||
)
|
)
|
||||||
# we use latest album's cover as artist image
|
# we use latest album's cover as artist image
|
||||||
latest_album = (
|
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 = [
|
metas = [
|
||||||
{"tag": "meta", "property": "og:url", "content": artist_url},
|
{"tag": "meta", "property": "og:url", "content": artist_url},
|
||||||
|
@ -234,7 +247,10 @@ def library_artist(request, pk, redirect_to_ap):
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
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)
|
.playable_by(None)
|
||||||
.exists()
|
.exists()
|
||||||
):
|
):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import collections
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
@ -9,7 +10,7 @@ from django.db import transaction
|
||||||
from django.db.models import F, Q
|
from django.db.models import F, Q
|
||||||
from django.dispatch import receiver
|
from django.dispatch import receiver
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from musicbrainzngs import ResponseError
|
from musicbrainzngs import NetworkError, ResponseError
|
||||||
from requests.exceptions import RequestException
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from funkwhale_api import musicbrainz
|
from funkwhale_api import musicbrainz
|
||||||
|
@ -226,7 +227,7 @@ def process_upload(upload, update_denormalization=True):
|
||||||
forced_values["artist"] = upload.library.channel.artist
|
forced_values["artist"] = upload.library.channel.artist
|
||||||
|
|
||||||
old_status = upload.import_status
|
old_status = upload.import_status
|
||||||
additional_data = {"upload_source": upload.source}
|
upload_source = {"upload_source": upload.source}
|
||||||
|
|
||||||
if use_file_metadata:
|
if use_file_metadata:
|
||||||
audio_file = upload.get_audio_file()
|
audio_file = upload.get_audio_file()
|
||||||
|
@ -256,11 +257,11 @@ def process_upload(upload, update_denormalization=True):
|
||||||
)
|
)
|
||||||
|
|
||||||
final_metadata = collections.ChainMap(
|
final_metadata = collections.ChainMap(
|
||||||
additional_data, serializer.validated_data, internal_config
|
upload_source, serializer.validated_data, internal_config
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
final_metadata = collections.ChainMap(
|
final_metadata = collections.ChainMap(
|
||||||
additional_data,
|
upload_source,
|
||||||
forced_values,
|
forced_values,
|
||||||
internal_config,
|
internal_config,
|
||||||
)
|
)
|
||||||
|
@ -270,8 +271,8 @@ def process_upload(upload, update_denormalization=True):
|
||||||
)
|
)
|
||||||
except UploadImportError as e:
|
except UploadImportError as e:
|
||||||
return fail_import(upload, e.code)
|
return fail_import(upload, e.code)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
fail_import(upload, "unknown_error")
|
fail_import(upload, "unknown_error", e)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
broadcast = getter(
|
broadcast = getter(
|
||||||
|
@ -395,38 +396,53 @@ def federation_audio_track_to_metadata(payload, references):
|
||||||
"cover_data": get_cover(payload["album"], "image"),
|
"cover_data": get_cover(payload["album"], "image"),
|
||||||
"release_date": payload["album"].get("released"),
|
"release_date": payload["album"].get("released"),
|
||||||
"tags": [t["name"] for t in payload["album"].get("tags", []) or []],
|
"tags": [t["name"] for t in payload["album"].get("tags", []) or []],
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"fid": a["id"],
|
"artist": {
|
||||||
"name": a["name"],
|
"fid": a["artist"]["id"],
|
||||||
"fdate": a["published"],
|
"name": a["artist"]["name"],
|
||||||
"cover_data": get_cover(a, "image"),
|
"fdate": a["artist"]["published"],
|
||||||
"description": a.get("description"),
|
"cover_data": get_cover(a["artist"], "image"),
|
||||||
"attributed_to": references.get(a.get("attributedTo")),
|
"description": a["artist"].get("description"),
|
||||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
"attributed_to": references.get(
|
||||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
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"],
|
||||||
}
|
}
|
||||||
for a in payload["album"]["artists"]
|
for a in payload["album"]["artist_credit"]
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"fid": a["id"],
|
"artist": {
|
||||||
"name": a["name"],
|
"fid": a["artist"]["id"],
|
||||||
"fdate": a["published"],
|
"name": a["artist"]["name"],
|
||||||
"description": a.get("description"),
|
"fdate": a["artist"]["published"],
|
||||||
"attributed_to": references.get(a.get("attributedTo")),
|
"description": a["artist"].get("description"),
|
||||||
"mbid": str(a["musicbrainzId"]) if a.get("musicbrainzId") else None,
|
"attributed_to": references.get(a["artist"].get("attributedTo")),
|
||||||
"tags": [t["name"] for t in a.get("tags", []) or []],
|
"mbid": str(a["artist"]["musicbrainzId"])
|
||||||
"cover_data": get_cover(a, "image"),
|
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"],
|
||||||
}
|
}
|
||||||
for a in payload["artists"]
|
for a in payload["artist_credit"]
|
||||||
],
|
],
|
||||||
# federation
|
# federation
|
||||||
"fid": payload["id"],
|
"fid": payload["id"],
|
||||||
"fdate": payload["published"],
|
"fdate": payload["published"],
|
||||||
"tags": [t["name"] for t in payload.get("tags", []) or []],
|
"tags": [t["name"] for t in payload.get("tags", []) or []],
|
||||||
}
|
}
|
||||||
|
|
||||||
return new_data
|
return new_data
|
||||||
|
|
||||||
|
|
||||||
|
@ -434,6 +450,7 @@ def get_owned_duplicates(upload, track):
|
||||||
"""
|
"""
|
||||||
Ensure we skip duplicate tracks to avoid wasting user/instance storage
|
Ensure we skip duplicate tracks to avoid wasting user/instance storage
|
||||||
"""
|
"""
|
||||||
|
|
||||||
owned_libraries = upload.library.actor.libraries.all()
|
owned_libraries = upload.library.actor.libraries.all()
|
||||||
return (
|
return (
|
||||||
models.Upload.objects.filter(
|
models.Upload.objects.filter(
|
||||||
|
@ -491,9 +508,11 @@ def sort_candidates(candidates, important_fields):
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def get_track_from_import_metadata(
|
def get_track_from_import_metadata(
|
||||||
data, update_cover=False, attributed_to=None, **forced_values
|
data, update_cover=False, attributed_to=None, query_mb=True, **forced_values
|
||||||
):
|
):
|
||||||
track = _get_track(data, attributed_to=attributed_to, **forced_values)
|
track = _get_track(
|
||||||
|
data, attributed_to=attributed_to, query_mb=query_mb, **forced_values
|
||||||
|
)
|
||||||
if update_cover and track and not track.album.attachment_cover:
|
if update_cover and track and not track.album.attachment_cover:
|
||||||
populate_album_cover(track.album, source=data.get("upload_source"))
|
populate_album_cover(track.album, source=data.get("upload_source"))
|
||||||
return track
|
return track
|
||||||
|
@ -505,7 +524,7 @@ def truncate(v, length):
|
||||||
return v[:length]
|
return v[:length]
|
||||||
|
|
||||||
|
|
||||||
def _get_track(data, attributed_to=None, **forced_values):
|
def _get_track(data, attributed_to=None, query_mb=True, **forced_values):
|
||||||
sync_mb_tag = preferences.get("music__sync_musicbrainz_tags")
|
sync_mb_tag = preferences.get("music__sync_musicbrainz_tags")
|
||||||
track_uuid = getter(data, "funkwhale", "track", "uuid")
|
track_uuid = getter(data, "funkwhale", "track", "uuid")
|
||||||
|
|
||||||
|
@ -548,64 +567,64 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
except IndexError:
|
except IndexError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# get / create artist and album artist
|
# get / create artist, artist_credit and album artist, album artist_credit
|
||||||
artists = getter(data, "artists", default=[])
|
album_artists_credits = None
|
||||||
|
artist_credit_data = getter(data, "artist_credit", default=[])
|
||||||
if "artist" in forced_values:
|
if "artist" in forced_values:
|
||||||
artist = forced_values["artist"]
|
artist = forced_values["artist"]
|
||||||
else:
|
query = Q(artist=artist)
|
||||||
artist_data = artists[0]
|
defaults = {
|
||||||
artist = get_artist(
|
"artist": artist,
|
||||||
artist_data, attributed_to=attributed_to, from_activity_id=from_activity_id
|
"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:
|
||||||
|
mbid = query_mb and (data.get("musicbrainz_id", None) or data.get("mbid", None))
|
||||||
|
try:
|
||||||
|
track_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||||
|
"recording",
|
||||||
|
mbid,
|
||||||
|
attributed_to=attributed_to,
|
||||||
|
from_activity_id=from_activity_id,
|
||||||
|
)
|
||||||
|
except (NoMbid, ResponseError, NetworkError):
|
||||||
|
track_artists_credits = (
|
||||||
|
get_or_create_artists_credits_from_artist_credit_metadata(
|
||||||
|
artist_credit_data,
|
||||||
|
attributed_to=attributed_to,
|
||||||
|
from_activity_id=from_activity_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if "album" in forced_values:
|
if "album" in forced_values:
|
||||||
album = forced_values["album"]
|
album = forced_values["album"]
|
||||||
|
album_artists_credits = track_artists_credits
|
||||||
else:
|
else:
|
||||||
if "artist" in forced_values:
|
if album_artists_credits:
|
||||||
album_artist = forced_values["artist"]
|
pass
|
||||||
else:
|
mbid = query_mb and (data.get("musicbrainz_albumid", None) or album_mbid)
|
||||||
album_artists = getter(data, "album", "artists", default=artists) or artists
|
try:
|
||||||
album_artist_data = album_artists[0]
|
album_artists_credits = get_or_create_artists_credits_from_musicbrainz(
|
||||||
album_artist_name = album_artist_data.get("name")
|
"release",
|
||||||
if album_artist_name == artist_name:
|
mbid,
|
||||||
album_artist = artist
|
attributed_to=attributed_to,
|
||||||
else:
|
from_activity_id=from_activity_id,
|
||||||
query = Q(name__iexact=album_artist_name)
|
)
|
||||||
album_artist_mbid = album_artist_data.get("mbid", None)
|
except (NoMbid, ResponseError, NetworkError):
|
||||||
album_artist_fid = album_artist_data.get("fid", None)
|
if album_artists := getter(data, "album", "artist_credit", default=None):
|
||||||
if album_artist_mbid:
|
album_artists_credits = (
|
||||||
query |= Q(mbid=album_artist_mbid)
|
get_or_create_artists_credits_from_artist_credit_metadata(
|
||||||
if album_artist_fid:
|
album_artists,
|
||||||
query |= Q(fid=album_artist_fid)
|
attributed_to=attributed_to,
|
||||||
defaults = {
|
from_activity_id=from_activity_id,
|
||||||
"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")
|
|
||||||
|
|
||||||
album_artist, created = get_best_candidate_or_create(
|
|
||||||
models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
|
||||||
)
|
)
|
||||||
if created:
|
else:
|
||||||
tags_models.add_tags(
|
album_artists_credits = track_artists_credits
|
||||||
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"),
|
|
||||||
)
|
|
||||||
|
|
||||||
# get / create album
|
# get / create album
|
||||||
if "album" in data:
|
if "album" in data:
|
||||||
|
@ -616,13 +635,15 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
if album_mbid:
|
if album_mbid:
|
||||||
query = Q(mbid=album_mbid)
|
query = Q(mbid=album_mbid)
|
||||||
else:
|
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:
|
if album_fid:
|
||||||
query |= Q(fid=album_fid)
|
query |= Q(fid=album_fid)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"title": album_title,
|
"title": album_title,
|
||||||
"artist": album_artist,
|
|
||||||
"mbid": album_mbid,
|
"mbid": album_mbid,
|
||||||
"release_date": album_data.get("release_date"),
|
"release_date": album_data.get("release_date"),
|
||||||
"fid": album_fid,
|
"fid": album_fid,
|
||||||
|
@ -635,6 +656,8 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
album, created = get_best_candidate_or_create(
|
album, created = get_best_candidate_or_create(
|
||||||
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
models.Album, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||||
)
|
)
|
||||||
|
album.artist_credit.set(album_artists_credits)
|
||||||
|
|
||||||
if created:
|
if created:
|
||||||
tags_models.add_tags(album, *album_data.get("tags", []))
|
tags_models.add_tags(album, *album_data.get("tags", []))
|
||||||
common_utils.attach_content(
|
common_utils.attach_content(
|
||||||
|
@ -682,7 +705,7 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
|
|
||||||
query = Q(
|
query = Q(
|
||||||
title__iexact=track_title,
|
title__iexact=track_title,
|
||||||
artist=artist,
|
artist_credit__in=track_artists_credits,
|
||||||
album=album,
|
album=album,
|
||||||
position=position,
|
position=position,
|
||||||
disc_number=disc_number,
|
disc_number=disc_number,
|
||||||
|
@ -695,17 +718,10 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
if track_fid:
|
if track_fid:
|
||||||
query |= Q(fid=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 = {
|
defaults = {
|
||||||
"title": track_title,
|
"title": track_title,
|
||||||
"album": album,
|
"album": album,
|
||||||
"mbid": track_mbid,
|
"mbid": track_mbid,
|
||||||
"artist": artist,
|
|
||||||
"position": position,
|
"position": position,
|
||||||
"disc_number": disc_number,
|
"disc_number": disc_number,
|
||||||
"fid": track_fid,
|
"fid": track_fid,
|
||||||
|
@ -732,27 +748,36 @@ def _get_track(data, attributed_to=None, **forced_values):
|
||||||
if sync_mb_tag and track_mbid:
|
if sync_mb_tag and track_mbid:
|
||||||
tags_tasks.sync_fw_item_tag_with_musicbrainz_tags(track)
|
tags_tasks.sync_fw_item_tag_with_musicbrainz_tags(track)
|
||||||
|
|
||||||
|
track.artist_credit.set(track_artists_credits)
|
||||||
return track
|
return track
|
||||||
|
|
||||||
|
|
||||||
def get_artist(artist_data, attributed_to, from_activity_id):
|
def get_or_create_artist(artist_data, attributed_to, from_activity_id):
|
||||||
sync_mb_tag = preferences.get("music__sync_musicbrainz_tags")
|
sync_mb_tag = preferences.get("music__sync_musicbrainz_tags")
|
||||||
artist_mbid = artist_data.get("mbid", None)
|
mbid = artist_data.get("artist", {}).get("mbid", None)
|
||||||
artist_fid = artist_data.get("fid", None)
|
fid = artist_data.get("artist", {}).get("fid", None)
|
||||||
artist_name = artist_data["name"]
|
name = artist_data.get("artist", {}).get("name", artist_data["credit"])
|
||||||
|
creation_date = artist_data.get("artist", {}).get("fdate", timezone.now())
|
||||||
|
description = artist_data.get("artist", {}).get("description", None)
|
||||||
|
attributed_to = artist_data.get("artist", {}).get("attributed_to", attributed_to)
|
||||||
|
tags = artist_data.get("artist", {}).get("tags", [])
|
||||||
|
cover = artist_data.get("artist", {}).get("cover_data", None)
|
||||||
|
|
||||||
if artist_mbid:
|
if mbid:
|
||||||
query = Q(mbid=artist_mbid)
|
query = Q(mbid=mbid)
|
||||||
else:
|
else:
|
||||||
query = Q(name__iexact=artist_name)
|
query = Q(name__iexact=name)
|
||||||
if artist_fid:
|
|
||||||
query |= Q(fid=artist_fid)
|
if fid:
|
||||||
|
query |= Q(fid=fid)
|
||||||
|
|
||||||
defaults = {
|
defaults = {
|
||||||
"name": artist_name,
|
"name": name,
|
||||||
"mbid": artist_mbid,
|
"mbid": mbid,
|
||||||
"fid": artist_fid,
|
"fid": fid,
|
||||||
"from_activity_id": from_activity_id,
|
"from_activity_id": from_activity_id,
|
||||||
"attributed_to": artist_data.get("attributed_to", attributed_to),
|
"attributed_to": attributed_to,
|
||||||
|
"creation_date": creation_date,
|
||||||
}
|
}
|
||||||
if artist_data.get("fdate"):
|
if artist_data.get("fdate"):
|
||||||
defaults["creation_date"] = artist_data.get("fdate")
|
defaults["creation_date"] = artist_data.get("fdate")
|
||||||
|
@ -761,19 +786,162 @@ def get_artist(artist_data, attributed_to, from_activity_id):
|
||||||
models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
models.Artist, query, defaults=defaults, sort_fields=["mbid", "fid"]
|
||||||
)
|
)
|
||||||
if created:
|
if created:
|
||||||
tags_models.add_tags(artist, *artist_data.get("tags", []))
|
tags_models.add_tags(artist, *tags)
|
||||||
common_utils.attach_content(
|
common_utils.attach_content(artist, "description", description)
|
||||||
artist, "description", artist_data.get("description")
|
common_utils.attach_file(artist, "attachment_cover", cover)
|
||||||
)
|
if sync_mb_tag and mbid:
|
||||||
common_utils.attach_file(
|
|
||||||
artist, "attachment_cover", artist_data.get("cover_data")
|
|
||||||
)
|
|
||||||
if sync_mb_tag and artist_mbid:
|
|
||||||
tags_tasks.sync_fw_item_tag_with_musicbrainz_tags(artist)
|
tags_tasks.sync_fw_item_tag_with_musicbrainz_tags(artist)
|
||||||
|
|
||||||
return artist
|
return artist
|
||||||
|
|
||||||
|
|
||||||
|
class NoMbid(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_or_create_artists_credits_from_musicbrainz(
|
||||||
|
mb_obj_type, mbid, attributed_to, from_activity_id
|
||||||
|
):
|
||||||
|
if not mbid:
|
||||||
|
raise NoMbid
|
||||||
|
|
||||||
|
try:
|
||||||
|
if mb_obj_type == "release":
|
||||||
|
mb_obj = musicbrainz.api.releases.get(mbid, includes=["artists"])
|
||||||
|
elif mb_obj_type == "recording":
|
||||||
|
mb_obj = musicbrainz.api.recordings.get(mbid, includes=["artists"])
|
||||||
|
except (ResponseError, NetworkError) as e:
|
||||||
|
logger.warning(
|
||||||
|
f"Couldn't get Musicbrainz information for {mb_obj_type} with {mbid} mbid \
|
||||||
|
because of the following exception : {e}"
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
artists_credits = []
|
||||||
|
acs = mb_obj.get("recording", mb_obj)["artist-credit"]
|
||||||
|
for i, ac in enumerate(acs):
|
||||||
|
if isinstance(ac, str):
|
||||||
|
continue
|
||||||
|
artist_mbid = ac["artist"]["id"]
|
||||||
|
artist_name = ac["artist"]["name"]
|
||||||
|
credit = ac.get("name", artist_name)
|
||||||
|
joinphrase = ac["joinphrase"]
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
)
|
||||||
|
|
||||||
|
# we could import artist 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):
|
||||||
|
credit = raw_artist_credit[0].strip()
|
||||||
|
if forced_joinphrase:
|
||||||
|
join_phrase = forced_joinphrase
|
||||||
|
else:
|
||||||
|
join_phrase = raw_artist_credit[1]
|
||||||
|
if join_phrase == "( " or join_phrase == ") ":
|
||||||
|
join_phrase = join_phrase.strip()
|
||||||
|
|
||||||
|
artists_credits_tuple.append(
|
||||||
|
(
|
||||||
|
credit,
|
||||||
|
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:
|
||||||
|
credit = forced_artist.name if forced_artist else artist_string
|
||||||
|
artists_credits_tuple.append(
|
||||||
|
(
|
||||||
|
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_credit_metadata(
|
||||||
|
artists_credits_data, attributed_to, from_activity_id
|
||||||
|
):
|
||||||
|
artists_credits = []
|
||||||
|
for i, ac in enumerate(artists_credits_data):
|
||||||
|
ac["artist"] = get_or_create_artist(ac, attributed_to, from_activity_id)
|
||||||
|
|
||||||
|
credit = ac.get("credit", ac["artist"].name)
|
||||||
|
query = (
|
||||||
|
Q(artist=ac["artist"])
|
||||||
|
& Q(credit=credit)
|
||||||
|
& Q(joinphrase=ac["joinphrase"])
|
||||||
|
& Q(index=i)
|
||||||
|
)
|
||||||
|
|
||||||
|
artist_credit, created = get_best_candidate_or_create(
|
||||||
|
models.ArtistCredit, query, ac, ["artist", "credit", "joinphrase"]
|
||||||
|
)
|
||||||
|
artists_credits.append(artist_credit)
|
||||||
|
|
||||||
|
return artists_credits
|
||||||
|
|
||||||
|
|
||||||
@receiver(signals.upload_import_status_updated)
|
@receiver(signals.upload_import_status_updated)
|
||||||
def broadcast_import_status_update_to_owner(old_status, new_status, upload, **kwargs):
|
def broadcast_import_status_update_to_owner(old_status, new_status, upload, **kwargs):
|
||||||
user = upload.library.actor.get_user()
|
user = upload.library.actor.get_user()
|
||||||
|
@ -895,7 +1063,7 @@ def get_prunable_albums():
|
||||||
|
|
||||||
|
|
||||||
def get_prunable_artists():
|
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):
|
def update_library_entity(obj, data):
|
||||||
|
@ -926,8 +1094,8 @@ UPDATE_CONFIG = {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"artists": {},
|
||||||
"album": {"title": {}, "mbid": {}, "release_date": {}},
|
"album": {"title": {}, "mbid": {}, "release_date": {}},
|
||||||
"artist": {"name": {}, "mbid": {}},
|
|
||||||
"album_artist": {"name": {}, "mbid": {}},
|
"album_artist": {"name": {}, "mbid": {}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -941,11 +1109,15 @@ def update_track_metadata(audio_metadata, track):
|
||||||
to_update = [
|
to_update = [
|
||||||
("track", track, lambda data: data),
|
("track", track, lambda data: data),
|
||||||
("album", track.album, lambda data: data["album"]),
|
("album", track.album, lambda data: data["album"]),
|
||||||
("artist", track.artist, lambda data: data["artists"][0]),
|
(
|
||||||
|
"artist_credit",
|
||||||
|
track.artist_credit.all(),
|
||||||
|
lambda data: data["artist_credit"],
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"album_artist",
|
"album_artist",
|
||||||
track.album.artist if track.album else None,
|
track.album.artist_credit.all() if track.album else None,
|
||||||
lambda data: data["album"]["artists"][0],
|
lambda data: data["album"]["artist_credit"],
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
for id, obj, data_getter in to_update:
|
for id, obj, data_getter in to_update:
|
||||||
|
@ -956,6 +1128,54 @@ def update_track_metadata(audio_metadata, track):
|
||||||
obj_data = data_getter(new_data)
|
obj_data = data_getter(new_data)
|
||||||
except IndexError:
|
except IndexError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if id == "artist_credit":
|
||||||
|
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(
|
||||||
|
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(
|
||||||
|
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():
|
for field, config in UPDATE_CONFIG[id].items():
|
||||||
getter = config.get(
|
getter = config.get(
|
||||||
"getter", lambda data, field: data[config.get("field", field)]
|
"getter", lambda data, field: data[config.get("field", field)]
|
||||||
|
@ -972,7 +1192,6 @@ def update_track_metadata(audio_metadata, track):
|
||||||
|
|
||||||
if obj_updated_fields:
|
if obj_updated_fields:
|
||||||
obj.save(update_fields=obj_updated_fields)
|
obj.save(update_fields=obj_updated_fields)
|
||||||
|
|
||||||
tags_models.set_tags(track, *new_data.get("tags", []))
|
tags_models.set_tags(track, *new_data.get("tags", []))
|
||||||
|
|
||||||
if track.album and "album" in new_data and new_data["album"].get("cover_data"):
|
if track.album and "album" in new_data and new_data["album"].get("cover_data"):
|
||||||
|
|
|
@ -162,3 +162,10 @@ def browse_dir(root, path):
|
||||||
files.append({"name": el, "dir": False})
|
files.append({"name": el, "dir": False})
|
||||||
|
|
||||||
return dirs + files
|
return dirs + files
|
||||||
|
|
||||||
|
|
||||||
|
def get_artist_credit_string(obj):
|
||||||
|
final_credit = ""
|
||||||
|
for ac in obj.artist_credit.all():
|
||||||
|
final_credit = final_credit + ac.credit + ac.joinphrase
|
||||||
|
return final_credit
|
||||||
|
|
|
@ -120,16 +120,12 @@ class ArtistViewSet(
|
||||||
):
|
):
|
||||||
queryset = (
|
queryset = (
|
||||||
models.Artist.objects.all()
|
models.Artist.objects.all()
|
||||||
.prefetch_related("attributed_to", "attachment_cover")
|
.select_related("attributed_to", "attachment_cover")
|
||||||
|
.order_by("-id")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"channel__actor",
|
"channel__actor",
|
||||||
Prefetch(
|
|
||||||
"tracks",
|
|
||||||
queryset=models.Track.objects.all(),
|
|
||||||
to_attr="_prefetched_tracks",
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.order_by("-id")
|
.annotate(_tracks_count=Count("artist_credit__tracks"))
|
||||||
)
|
)
|
||||||
serializer_class = serializers.ArtistWithAlbumsSerializer
|
serializer_class = serializers.ArtistWithAlbumsSerializer
|
||||||
permission_classes = [oauth_permissions.ScopePermission]
|
permission_classes = [oauth_permissions.ScopePermission]
|
||||||
|
@ -166,12 +162,12 @@ class ArtistViewSet(
|
||||||
utils.get_actor_from_request(self.request)
|
utils.get_actor_from_request(self.request)
|
||||||
)
|
)
|
||||||
return queryset.prefetch_related(
|
return queryset.prefetch_related(
|
||||||
Prefetch("albums", queryset=albums), TAG_PREFETCH
|
Prefetch("artist_credit__albums", queryset=albums), TAG_PREFETCH
|
||||||
)
|
)
|
||||||
|
|
||||||
libraries = get_libraries(
|
libraries = get_libraries(
|
||||||
lambda o, uploads: uploads.filter(
|
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)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -186,8 +182,9 @@ class AlbumViewSet(
|
||||||
queryset = (
|
queryset = (
|
||||||
models.Album.objects.all()
|
models.Album.objects.all()
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
.prefetch_related("artist__channel", "attributed_to", "attachment_cover")
|
.select_related("attributed_to", "attachment_cover")
|
||||||
)
|
.prefetch_related("artist_credit__artist__channel")
|
||||||
|
).distinct()
|
||||||
serializer_class = serializers.AlbumSerializer
|
serializer_class = serializers.AlbumSerializer
|
||||||
permission_classes = [oauth_permissions.ScopePermission]
|
permission_classes = [oauth_permissions.ScopePermission]
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
|
@ -219,8 +216,8 @@ class AlbumViewSet(
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.action in ["destroy"]:
|
if self.action in ["destroy"]:
|
||||||
queryset = queryset.exclude(artist__channel=None).filter(
|
queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
|
||||||
artist__attributed_to=self.request.user.actor
|
artist_credit__artist__attributed_to=self.request.user.actor
|
||||||
)
|
)
|
||||||
|
|
||||||
tracks = models.Track.objects.all().prefetch_related("album")
|
tracks = models.Track.objects.all().prefetch_related("album")
|
||||||
|
@ -246,6 +243,54 @@ class AlbumViewSet(
|
||||||
)
|
)
|
||||||
models.Album.objects.filter(pk=instance.pk).delete()
|
models.Album.objects.filter(pk=instance.pk).delete()
|
||||||
|
|
||||||
|
@transaction.atomic
|
||||||
|
def create(self, request, *args, **kwargs):
|
||||||
|
request_data = request.data.copy()
|
||||||
|
|
||||||
|
if mbid := request_data.get("musicbrainz_albumid", None) or request_data.get(
|
||||||
|
"mbid", None
|
||||||
|
):
|
||||||
|
artist_credit = tasks.get_or_create_artists_credits_from_musicbrainz(
|
||||||
|
"release",
|
||||||
|
mbid,
|
||||||
|
attributed_to=request.user.actor,
|
||||||
|
from_activity_id=None,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
artist_data = request_data.pop("artist", False)
|
||||||
|
artist_credit_data = request_data.pop("artist_credit", False)
|
||||||
|
if not artist_data and not artist_credit_data:
|
||||||
|
return Response({}, status=400)
|
||||||
|
if artist_data:
|
||||||
|
try:
|
||||||
|
artist = models.Artist.objects.get(pk=artist_data)
|
||||||
|
except models.Artist.DoesNotExist:
|
||||||
|
return Response({"détail": "artist id not found"}, status=400)
|
||||||
|
|
||||||
|
artist_credit, created = models.ArtistCredit.objects.get_or_create(
|
||||||
|
**{
|
||||||
|
"artist": artist,
|
||||||
|
"credit": artist.name,
|
||||||
|
"joinphrase": "",
|
||||||
|
"index": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
elif artist_credit_data:
|
||||||
|
try:
|
||||||
|
artist_credit = models.ArtistCredit.objects.get(
|
||||||
|
pk=artist_credit_data
|
||||||
|
)
|
||||||
|
except models.ArtistCredit.DoesNotExist:
|
||||||
|
return Response(
|
||||||
|
{"détail": "artist_credit id not found"}, status=400
|
||||||
|
)
|
||||||
|
|
||||||
|
request_data["artist_credit"] = [artist_credit.pk]
|
||||||
|
serializer = self.get_serializer(data=request_data)
|
||||||
|
serializer.is_valid(raise_exception=True)
|
||||||
|
self.perform_create(serializer)
|
||||||
|
return Response(serializer.data, status=204)
|
||||||
|
|
||||||
|
|
||||||
class LibraryViewSet(
|
class LibraryViewSet(
|
||||||
mixins.CreateModelMixin,
|
mixins.CreateModelMixin,
|
||||||
|
@ -404,7 +449,7 @@ class TrackViewSet(
|
||||||
.for_nested_serialization()
|
.for_nested_serialization()
|
||||||
.prefetch_related("attributed_to", "attachment_cover")
|
.prefetch_related("attributed_to", "attachment_cover")
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
)
|
).distinct()
|
||||||
serializer_class = serializers.TrackSerializer
|
serializer_class = serializers.TrackSerializer
|
||||||
permission_classes = [oauth_permissions.ScopePermission]
|
permission_classes = [oauth_permissions.ScopePermission]
|
||||||
required_scope = "libraries"
|
required_scope = "libraries"
|
||||||
|
@ -426,8 +471,8 @@ class TrackViewSet(
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = super().get_queryset()
|
queryset = super().get_queryset()
|
||||||
if self.action in ["destroy"]:
|
if self.action in ["destroy"]:
|
||||||
queryset = queryset.exclude(artist__channel=None).filter(
|
queryset = queryset.exclude(artist_credit__artist__channel=None).filter(
|
||||||
artist__attributed_to=self.request.user.actor
|
artist_credit__artist__attributed_to=self.request.user.actor
|
||||||
)
|
)
|
||||||
filter_favorites = self.request.GET.get("favorites", None)
|
filter_favorites = self.request.GET.get("favorites", None)
|
||||||
user = self.request.user
|
user = self.request.user
|
||||||
|
@ -653,7 +698,9 @@ class ListenMixin(mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
||||||
|
|
||||||
def handle_stream(track, request, download, explicit_file, format, max_bitrate):
|
def handle_stream(track, request, download, explicit_file, format, max_bitrate):
|
||||||
actor = utils.get_actor_from_request(request)
|
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:
|
if explicit_file:
|
||||||
queryset = queryset.filter(uuid=explicit_file)
|
queryset = queryset.filter(uuid=explicit_file)
|
||||||
queryset = queryset.playable_by(actor)
|
queryset = queryset.playable_by(actor)
|
||||||
|
@ -723,8 +770,8 @@ class UploadViewSet(
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"library__actor",
|
"library__actor",
|
||||||
"track__artist",
|
"track__artist_credit",
|
||||||
"track__album__artist",
|
"track__album__artist_credit",
|
||||||
"track__attachment_cover",
|
"track__attachment_cover",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -743,7 +790,7 @@ class UploadViewSet(
|
||||||
"import_date",
|
"import_date",
|
||||||
"bitrate",
|
"bitrate",
|
||||||
"size",
|
"size",
|
||||||
"artist__name",
|
"artist_credit__artist__name",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
@ -846,20 +893,24 @@ class Search(views.APIView):
|
||||||
def get_tracks(self, query):
|
def get_tracks(self, query):
|
||||||
query_obj = utils.get_fts_query(
|
query_obj = utils.get_fts_query(
|
||||||
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,
|
model=models.Track,
|
||||||
)
|
)
|
||||||
qs = (
|
qs = (
|
||||||
models.Track.objects.all()
|
models.Track.objects.all()
|
||||||
.filter(query_obj)
|
.filter(query_obj)
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
"artist",
|
"artist_credit",
|
||||||
"attributed_to",
|
"attributed_to",
|
||||||
Prefetch(
|
Prefetch(
|
||||||
"album",
|
"album",
|
||||||
queryset=models.Album.objects.select_related(
|
queryset=models.Album.objects.select_related(
|
||||||
"artist", "attachment_cover", "attributed_to"
|
"attachment_cover", "attributed_to"
|
||||||
).prefetch_related("tracks"),
|
).prefetch_related("tracks", "artist_credit"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -867,13 +918,15 @@ class Search(views.APIView):
|
||||||
|
|
||||||
def get_albums(self, query):
|
def get_albums(self, query):
|
||||||
query_obj = utils.get_fts_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 = (
|
qs = (
|
||||||
models.Album.objects.all()
|
models.Album.objects.all()
|
||||||
.filter(query_obj)
|
.filter(query_obj)
|
||||||
.select_related("artist", "attachment_cover", "attributed_to")
|
.select_related("attachment_cover", "attributed_to")
|
||||||
.prefetch_related("tracks__artist")
|
.prefetch_related("tracks__artist_credit", "artist_credit")
|
||||||
)
|
)
|
||||||
return common_utils.order_for_search(qs, "title")[: self.max_results]
|
return common_utils.order_for_search(qs, "title")[: self.max_results]
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from funkwhale_api import __version__
|
||||||
_api = musicbrainzngs
|
_api = musicbrainzngs
|
||||||
_api.set_useragent("funkwhale", str(__version__), settings.FUNKWHALE_URL)
|
_api.set_useragent("funkwhale", str(__version__), settings.FUNKWHALE_URL)
|
||||||
_api.set_hostname(settings.MUSICBRAINZ_HOSTNAME)
|
_api.set_hostname(settings.MUSICBRAINZ_HOSTNAME)
|
||||||
|
_api.set_format(fmt="json")
|
||||||
|
|
||||||
|
|
||||||
def clean_artist_search(query, **kwargs):
|
def clean_artist_search(query, **kwargs):
|
||||||
|
|
|
@ -22,7 +22,7 @@ class PlaylistFilter(filters.FilterSet):
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
artist = filters.ModelChoiceFilter(
|
artist = filters.ModelChoiceFilter(
|
||||||
"playlist_tracks__track__artist",
|
"playlist_tracks__track__artist_credit__artist",
|
||||||
queryset=music_models.Artist.objects.all(),
|
queryset=music_models.Artist.objects.all(),
|
||||||
distinct=True,
|
distinct=True,
|
||||||
)
|
)
|
||||||
|
|
|
@ -191,8 +191,11 @@ class Playlist(models.Model):
|
||||||
class PlaylistTrackQuerySet(models.QuerySet):
|
class PlaylistTrackQuerySet(models.QuerySet):
|
||||||
def for_nested_serialization(self, actor=None):
|
def for_nested_serialization(self, actor=None):
|
||||||
tracks = music_models.Track.objects.with_playable_uploads(actor)
|
tracks = music_models.Track.objects.with_playable_uploads(actor)
|
||||||
tracks = tracks.select_related(
|
tracks = tracks.prefetch_related(
|
||||||
"artist", "album__artist", "album__attachment_cover", "attributed_to"
|
"artist_credit__artist",
|
||||||
|
"album__artist_credit__artist",
|
||||||
|
"album__attachment_cover",
|
||||||
|
"attributed_to",
|
||||||
)
|
)
|
||||||
return self.prefetch_related(
|
return self.prefetch_related(
|
||||||
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
|
models.Prefetch("track", queryset=tracks, to_attr="_prefetched_track")
|
||||||
|
|
|
@ -95,7 +95,9 @@ class PlaylistSerializer(serializers.ModelSerializer):
|
||||||
covers = []
|
covers = []
|
||||||
max_covers = 5
|
max_covers = 5
|
||||||
for plt in plts:
|
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
|
continue
|
||||||
url = plt.track.album.attachment_cover.download_url_medium_square_crop
|
url = plt.track.album.attachment_cover.download_url_medium_square_crop
|
||||||
if url in covers:
|
if url in covers:
|
||||||
|
|
|
@ -158,7 +158,7 @@ class ArtistFilter(RadioFilter):
|
||||||
return filter_config
|
return filter_config
|
||||||
|
|
||||||
def get_query(self, candidates, ids, **kwargs):
|
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):
|
def validate(self, config):
|
||||||
super().validate(config)
|
super().validate(config)
|
||||||
|
@ -199,8 +199,8 @@ class TagFilter(RadioFilter):
|
||||||
def get_query(self, candidates, names, **kwargs):
|
def get_query(self, candidates, names, **kwargs):
|
||||||
return (
|
return (
|
||||||
Q(tagged_items__tag__name__in=names)
|
Q(tagged_items__tag__name__in=names)
|
||||||
| Q(artist__tagged_items__tag__name__in=names)
|
| Q(artist_credit__artist__tagged_items__tag__name__in=names)
|
||||||
| Q(album__tagged_items__tag__name__in=names)
|
| Q(artist_credit__albums__tagged_items__tag__name__in=names)
|
||||||
)
|
)
|
||||||
|
|
||||||
def clean_config(self, filter_config):
|
def clean_config(self, filter_config):
|
||||||
|
|
|
@ -66,13 +66,21 @@ class SessionRadio(SimpleRadio):
|
||||||
return (
|
return (
|
||||||
Track.objects.all()
|
Track.objects.all()
|
||||||
.with_playable_uploads(actor=None)
|
.with_playable_uploads(actor=None)
|
||||||
.select_related("artist", "album__artist", "attributed_to")
|
.prefetch_related(
|
||||||
|
"artist_credit__artist",
|
||||||
|
"album__artist_credit__artist",
|
||||||
|
"attributed_to",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
qs = (
|
qs = (
|
||||||
Track.objects.all()
|
Track.objects.all()
|
||||||
.with_playable_uploads(self.session.user.actor)
|
.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(
|
query = moderation_filters.get_filtered_content_query(
|
||||||
|
@ -124,7 +132,7 @@ class SessionRadio(SimpleRadio):
|
||||||
class RandomRadio(SessionRadio):
|
class RandomRadio(SessionRadio):
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**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")
|
@registry.register(name="random_library")
|
||||||
|
@ -134,7 +142,9 @@ class RandomLibraryRadio(SessionRadio):
|
||||||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||||
"id", flat=True
|
"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("?")
|
return qs.filter(query).order_by("?")
|
||||||
|
|
||||||
|
|
||||||
|
@ -149,7 +159,9 @@ class FavoritesRadio(SessionRadio):
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True)
|
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")
|
@registry.register(name="custom")
|
||||||
|
@ -240,8 +252,8 @@ class TagRadio(RelatedObjectRadio):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
query = (
|
query = (
|
||||||
Q(tagged_items__tag=self.session.related_object)
|
Q(tagged_items__tag=self.session.related_object)
|
||||||
| Q(artist__tagged_items__tag=self.session.related_object)
|
| Q(artist_credit__artist__tagged_items__tag=self.session.related_object)
|
||||||
| Q(album__tagged_items__tag=self.session.related_object)
|
| Q(artist_credit__albums__tagged_items__tag=self.session.related_object)
|
||||||
)
|
)
|
||||||
return qs.filter(query)
|
return qs.filter(query)
|
||||||
|
|
||||||
|
@ -323,7 +335,7 @@ class ArtistRadio(RelatedObjectRadio):
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**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")
|
@registry.register(name="less-listened")
|
||||||
|
@ -336,7 +348,7 @@ class LessListenedRadio(SessionRadio):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
||||||
return (
|
return (
|
||||||
qs.filter(artist__content_category="music")
|
qs.filter(artist_credit__artist__content_category="music")
|
||||||
.exclude(pk__in=listened)
|
.exclude(pk__in=listened)
|
||||||
.order_by("?")
|
.order_by("?")
|
||||||
)
|
)
|
||||||
|
@ -354,7 +366,9 @@ class LessListenedLibraryRadio(SessionRadio):
|
||||||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||||
"id", flat=True
|
"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("?")
|
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)
|
date = datetime.date.today() - datetime.timedelta(days=30)
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
return qs.filter(
|
return qs.filter(
|
||||||
Q(artist__content_category="music"),
|
Q(artist_credit__artist__content_category="music"),
|
||||||
Q(creation_date__gt=date),
|
Q(creation_date__gt=date),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,9 @@ class SessionRadio(SimpleRadio):
|
||||||
qs = (
|
qs = (
|
||||||
Track.objects.all()
|
Track.objects.all()
|
||||||
.with_playable_uploads(actor=actor)
|
.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(
|
query = moderation_filters.get_filtered_content_query(
|
||||||
|
@ -164,7 +166,7 @@ class SessionRadio(SimpleRadio):
|
||||||
class RandomRadio(SessionRadio):
|
class RandomRadio(SessionRadio):
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**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")
|
@registry.register(name="random_library")
|
||||||
|
@ -174,7 +176,9 @@ class RandomLibraryRadio(SessionRadio):
|
||||||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||||
"id", flat=True
|
"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("?")
|
return qs.filter(query).order_by("?")
|
||||||
|
|
||||||
|
|
||||||
|
@ -189,7 +193,9 @@ class FavoritesRadio(SessionRadio):
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
track_ids = kwargs["user"].track_favorites.all().values_list("track", flat=True)
|
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")
|
@registry.register(name="custom")
|
||||||
|
@ -363,7 +369,7 @@ class ArtistRadio(RelatedObjectRadio):
|
||||||
|
|
||||||
def get_queryset(self, **kwargs):
|
def get_queryset(self, **kwargs):
|
||||||
qs = super().get_queryset(**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")
|
@registry.register(name="less-listened")
|
||||||
|
@ -376,7 +382,7 @@ class LessListenedRadio(SessionRadio):
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
listened = self.session.user.listenings.all().values_list("track", flat=True)
|
||||||
return (
|
return (
|
||||||
qs.filter(artist__content_category="music")
|
qs.filter(artist_credit__artist__content_category="music")
|
||||||
.exclude(pk__in=listened)
|
.exclude(pk__in=listened)
|
||||||
.order_by("?")
|
.order_by("?")
|
||||||
)
|
)
|
||||||
|
@ -394,7 +400,9 @@ class LessListenedLibraryRadio(SessionRadio):
|
||||||
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
tracks_ids = self.session.user.actor.attributed_tracks.all().values_list(
|
||||||
"id", flat=True
|
"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("?")
|
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)
|
date = datetime.date.today() - datetime.timedelta(days=30)
|
||||||
qs = super().get_queryset(**kwargs)
|
qs = super().get_queryset(**kwargs)
|
||||||
return qs.filter(
|
return qs.filter(
|
||||||
Q(artist__content_category="music"),
|
Q(artist_credit__artist__content_category="music"),
|
||||||
Q(creation_date__gt=date),
|
Q(creation_date__gt=date),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ class AlbumList2FilterSet(filters.FilterSet):
|
||||||
ORDERING = {
|
ORDERING = {
|
||||||
"random": "?",
|
"random": "?",
|
||||||
"newest": "-creation_date",
|
"newest": "-creation_date",
|
||||||
"alphabeticalByArtist": "artist__name",
|
"alphabeticalByArtist": "artist_credit__artist__name",
|
||||||
"alphabeticalByName": "title",
|
"alphabeticalByName": "title",
|
||||||
}
|
}
|
||||||
if value not in ORDERING:
|
if value not in ORDERING:
|
||||||
|
|
|
@ -36,7 +36,7 @@ def get_valid_filepart(s):
|
||||||
|
|
||||||
def get_track_path(track, suffix):
|
def get_track_path(track, suffix):
|
||||||
parts = []
|
parts = []
|
||||||
parts.append(get_valid_filepart(track.artist.name))
|
parts.append(get_valid_filepart(track.get_artist_credit_string))
|
||||||
if track.album:
|
if track.album:
|
||||||
parts.append(get_valid_filepart(track.album.title))
|
parts.append(get_valid_filepart(track.album.title))
|
||||||
track_part = get_valid_filepart(track.title) + "." + suffix
|
track_part = get_valid_filepart(track.title) + "." + suffix
|
||||||
|
@ -79,7 +79,7 @@ class GetArtistsSerializer(serializers.Serializer):
|
||||||
|
|
||||||
class GetArtistSerializer(serializers.Serializer):
|
class GetArtistSerializer(serializers.Serializer):
|
||||||
def to_representation(self, artist):
|
def to_representation(self, artist):
|
||||||
albums = artist.albums.prefetch_related("tracks__uploads")
|
albums = artist.artist_credit.albums().prefetch_related("tracks__uploads")
|
||||||
payload = {
|
payload = {
|
||||||
"id": artist.pk,
|
"id": artist.pk,
|
||||||
"name": artist.name,
|
"name": artist.name,
|
||||||
|
@ -128,7 +128,7 @@ def get_track_data(album, track, upload):
|
||||||
"isDir": "false",
|
"isDir": "false",
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
"album": album.title if album else "",
|
"album": album.title if album else "",
|
||||||
"artist": track.artist.name,
|
"artist": track.get_artist_credit_string,
|
||||||
"track": track.position or 1,
|
"track": track.position or 1,
|
||||||
"discNumber": track.disc_number 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
|
# Ugly fallback to mp3 but some subsonic clients fail if the value is empty or null, and we don't always
|
||||||
|
@ -144,7 +144,11 @@ def get_track_data(album, track, upload):
|
||||||
"duration": upload.duration or 0,
|
"duration": upload.duration or 0,
|
||||||
"created": to_subsonic_date(track.creation_date),
|
"created": to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk if album else "",
|
"albumId": album.pk if album else "",
|
||||||
"artistId": album.artist.pk if album else track.artist.pk,
|
"artistId": (
|
||||||
|
album.artist_credit.all()[0].artist.pk
|
||||||
|
if album
|
||||||
|
else track.artist_credit.all()[0].artist.pk
|
||||||
|
),
|
||||||
"type": "music",
|
"type": "music",
|
||||||
"mediaType": "song",
|
"mediaType": "song",
|
||||||
"musicBrainzId": str(track.mbid or ""),
|
"musicBrainzId": str(track.mbid or ""),
|
||||||
|
@ -165,9 +169,9 @@ def get_track_data(album, track, upload):
|
||||||
def get_album2_data(album):
|
def get_album2_data(album):
|
||||||
payload = {
|
payload = {
|
||||||
"id": album.id,
|
"id": album.id,
|
||||||
"artistId": album.artist.id,
|
"artistId": album.artist_credit.all()[0].artist.pk,
|
||||||
"name": album.title,
|
"name": album.title,
|
||||||
"artist": album.artist.name,
|
"artist": album.get_artist_credit_string,
|
||||||
"created": to_subsonic_date(album.creation_date),
|
"created": to_subsonic_date(album.creation_date),
|
||||||
"duration": album.duration,
|
"duration": album.duration,
|
||||||
"playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0,
|
"playCount": album.tracks.aggregate(l=Count("listenings"))["l"] or 0,
|
||||||
|
@ -226,7 +230,7 @@ def get_starred_tracks_data(favorites):
|
||||||
by_track_id = {f.track_id: f for f in favorites}
|
by_track_id = {f.track_id: f for f in favorites}
|
||||||
tracks = (
|
tracks = (
|
||||||
music_models.Track.objects.filter(pk__in=by_track_id.keys())
|
music_models.Track.objects.filter(pk__in=by_track_id.keys())
|
||||||
.select_related("album__artist")
|
.prefetch_related("album__artist_credit__artist")
|
||||||
.prefetch_related("uploads")
|
.prefetch_related("uploads")
|
||||||
)
|
)
|
||||||
tracks = tracks.order_by("-creation_date")
|
tracks = tracks.order_by("-creation_date")
|
||||||
|
@ -261,7 +265,7 @@ def get_playlist_data(playlist):
|
||||||
def get_playlist_detail_data(playlist):
|
def get_playlist_detail_data(playlist):
|
||||||
data = get_playlist_data(playlist)
|
data = get_playlist_data(playlist)
|
||||||
qs = (
|
qs = (
|
||||||
playlist.playlist_tracks.select_related("track__album__artist")
|
playlist.playlist_tracks.prefetch_related("track__album__artist_credit__artist")
|
||||||
.prefetch_related("track__uploads")
|
.prefetch_related("track__uploads")
|
||||||
.order_by("index")
|
.order_by("index")
|
||||||
)
|
)
|
||||||
|
|
|
@ -278,7 +278,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
|
detail=False, methods=["get", "post"], url_name="get_album", url_path="getAlbum"
|
||||||
)
|
)
|
||||||
@find_object(
|
@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,
|
filter_playable=True,
|
||||||
)
|
)
|
||||||
def get_album(self, request, *args, **kwargs):
|
def get_album(self, request, *args, **kwargs):
|
||||||
|
@ -292,7 +294,9 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
def stream(self, request, *args, **kwargs):
|
def stream(self, request, *args, **kwargs):
|
||||||
data = request.GET or request.POST
|
data = request.GET or request.POST
|
||||||
track = kwargs.pop("obj")
|
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)
|
sorted_uploads = music_serializers.sort_uploads_for_listen(queryset)
|
||||||
|
|
||||||
if not sorted_uploads:
|
if not sorted_uploads:
|
||||||
|
@ -416,9 +420,11 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
queryset.playable_by(actor)
|
queryset.playable_by(actor)
|
||||||
.filter(
|
.filter(
|
||||||
Q(tagged_items__tag__name=genre)
|
Q(tagged_items__tag__name=genre)
|
||||||
| Q(artist__tagged_items__tag__name=genre)
|
| Q(artist_credit__artist__tagged_items__tag__name=genre)
|
||||||
| Q(album__artist__tagged_items__tag__name=genre)
|
| Q(
|
||||||
| Q(album__tagged_items__tag__name=genre)
|
artist_credit__albums__artist_credit__artist__tagged_items__tag__name=genre
|
||||||
|
)
|
||||||
|
| Q(artist_credit__albums__tagged_items__tag__name=genre)
|
||||||
)
|
)
|
||||||
.prefetch_related("uploads")
|
.prefetch_related("uploads")
|
||||||
.distinct()
|
.distinct()
|
||||||
|
@ -457,7 +463,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
)
|
)
|
||||||
.with_tracks_count()
|
.with_tracks_count()
|
||||||
.with_duration()
|
.with_duration()
|
||||||
.order_by("artist__name")
|
.order_by("artist_credit__artist__name")
|
||||||
)
|
)
|
||||||
data = request.GET or request.POST
|
data = request.GET or request.POST
|
||||||
filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
|
filterset = filters.AlbumList2FilterSet(data, queryset=queryset)
|
||||||
|
@ -480,7 +486,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
genre = data.get("genre")
|
genre = data.get("genre")
|
||||||
queryset = queryset.filter(
|
queryset = queryset.filter(
|
||||||
Q(tagged_items__tag__name=genre)
|
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":
|
elif type == "byYear":
|
||||||
try:
|
try:
|
||||||
|
@ -549,7 +555,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
"queryset": (
|
"queryset": (
|
||||||
music_models.Album.objects.with_duration()
|
music_models.Album.objects.with_duration()
|
||||||
.with_tracks_count()
|
.with_tracks_count()
|
||||||
.select_related("artist")
|
.prefetch_related("artist_credit__artist")
|
||||||
),
|
),
|
||||||
"serializer": serializers.get_album_list2_data,
|
"serializer": serializers.get_album_list2_data,
|
||||||
},
|
},
|
||||||
|
@ -559,7 +565,7 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
"queryset": (
|
"queryset": (
|
||||||
music_models.Track.objects.prefetch_related(
|
music_models.Track.objects.prefetch_related(
|
||||||
"uploads"
|
"uploads"
|
||||||
).select_related("album__artist")
|
).prefetch_related("artist_credit__albums__artist_credit__artist")
|
||||||
),
|
),
|
||||||
"serializer": serializers.get_song_list_data,
|
"serializer": serializers.get_song_list_data,
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,19 +22,21 @@ def get_tags_from_foreign_key(
|
||||||
"""
|
"""
|
||||||
data = {}
|
data = {}
|
||||||
objs = foreign_key_model.objects.filter(
|
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")
|
).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():
|
for obj in objs.iterator():
|
||||||
# loop on all objects, store the objs tags + counter on the corresponding foreign key
|
for ac in obj.artist_credit.all():
|
||||||
row_data = data.setdefault(
|
# loop on all objects, store the objs tags + counter on the corresponding foreign key
|
||||||
getattr(obj, f"{foreign_key_attr}_id"),
|
row_data = data.setdefault(
|
||||||
{"total_objs": 0, "tags": []},
|
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["total_objs"] += 1
|
||||||
row_data["tags"].append(ti.tag_id)
|
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
|
# now, keep only tags that are present on all objects, i.e tags where the count
|
||||||
# matches total_objs
|
# matches total_objs
|
||||||
|
|
|
@ -55,7 +55,7 @@ def add_tracks_to_index(tracks_pk):
|
||||||
document = dict()
|
document = dict()
|
||||||
document["pk"] = track.pk
|
document["pk"] = track.pk
|
||||||
document["combined"] = utils.delete_non_alnum_characters(
|
document["combined"] = utils.delete_non_alnum_characters(
|
||||||
track.artist.name + track.title
|
track.get_artist_credit_string + track.title
|
||||||
)
|
)
|
||||||
documents.append(document)
|
documents.append(document)
|
||||||
|
|
||||||
|
|
|
@ -678,7 +678,7 @@ def test_rss_feed_item_serializer_create(factories):
|
||||||
assert upload.duration == 1357
|
assert upload.duration == 1357
|
||||||
assert upload.mimetype == "audio/mpeg"
|
assert upload.mimetype == "audio/mpeg"
|
||||||
assert upload.track.uuid == expected_uuid
|
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.copyright == "test something"
|
||||||
assert upload.track.position == 33
|
assert upload.track.position == 33
|
||||||
assert upload.track.disc_number == 2
|
assert upload.track.disc_number == 2
|
||||||
|
@ -702,7 +702,7 @@ def test_rss_feed_item_serializer_update(factories):
|
||||||
track__uuid=expected_uuid,
|
track__uuid=expected_uuid,
|
||||||
source="https://file.domain/audio.mp3",
|
source="https://file.domain/audio.mp3",
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
)
|
)
|
||||||
track = upload.track
|
track = upload.track
|
||||||
|
|
||||||
|
@ -748,7 +748,7 @@ def test_rss_feed_item_serializer_update(factories):
|
||||||
assert upload.duration == 1357
|
assert upload.duration == 1357
|
||||||
assert upload.mimetype == "audio/mpeg"
|
assert upload.mimetype == "audio/mpeg"
|
||||||
assert upload.track.uuid == expected_uuid
|
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.copyright == "test something"
|
||||||
assert upload.track.position == 33
|
assert upload.track.position == 33
|
||||||
assert upload.track.disc_number == 2
|
assert upload.track.disc_number == 2
|
||||||
|
|
|
@ -1,115 +1,115 @@
|
||||||
import pytest
|
import pytest
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
|
|
||||||
from funkwhale_api.cli import library, main, users
|
from funkwhale_api.cli import library, main
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"cmd, args, handlers",
|
"cmd, args, handlers",
|
||||||
[
|
[
|
||||||
(
|
# (
|
||||||
("users", "create"),
|
# ("users", "create"),
|
||||||
(
|
# (
|
||||||
"--username",
|
# "--username",
|
||||||
"testuser",
|
# "testuser",
|
||||||
"--password",
|
# "--password",
|
||||||
"testpassword",
|
# "testpassword",
|
||||||
"--email",
|
# "--email",
|
||||||
"test@hello.com",
|
# "test@hello.com",
|
||||||
"--upload-quota",
|
# "--upload-quota",
|
||||||
"35",
|
# "35",
|
||||||
"--permission",
|
# "--permission",
|
||||||
"library",
|
# "library",
|
||||||
"--permission",
|
# "--permission",
|
||||||
"moderation",
|
# "moderation",
|
||||||
"--staff",
|
# "--staff",
|
||||||
"--superuser",
|
# "--superuser",
|
||||||
),
|
# ),
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
users,
|
# users,
|
||||||
"handler_create_user",
|
# "handler_create_user",
|
||||||
{
|
# {
|
||||||
"username": "testuser",
|
# "username": "testuser",
|
||||||
"password": "testpassword",
|
# "password": "testpassword",
|
||||||
"email": "test@hello.com",
|
# "email": "test@hello.com",
|
||||||
"upload_quota": 35,
|
# "upload_quota": 35,
|
||||||
"permissions": ("library", "moderation"),
|
# "permissions": ("library", "moderation"),
|
||||||
"is_staff": True,
|
# "is_staff": True,
|
||||||
"is_superuser": True,
|
# "is_superuser": True,
|
||||||
},
|
# },
|
||||||
)
|
# )
|
||||||
],
|
# ],
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
("users", "rm"),
|
# ("users", "rm"),
|
||||||
("testuser1", "testuser2", "--no-input"),
|
# ("testuser1", "testuser2", "--no-input"),
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
users,
|
# users,
|
||||||
"handler_delete_user",
|
# "handler_delete_user",
|
||||||
{"usernames": ("testuser1", "testuser2"), "soft": True},
|
# {"usernames": ("testuser1", "testuser2"), "soft": True},
|
||||||
)
|
# )
|
||||||
],
|
# ],
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
("users", "rm"),
|
# ("users", "rm"),
|
||||||
(
|
# (
|
||||||
"testuser1",
|
# "testuser1",
|
||||||
"testuser2",
|
# "testuser2",
|
||||||
"--no-input",
|
# "--no-input",
|
||||||
"--hard",
|
# "--hard",
|
||||||
),
|
# ),
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
users,
|
# users,
|
||||||
"handler_delete_user",
|
# "handler_delete_user",
|
||||||
{"usernames": ("testuser1", "testuser2"), "soft": False},
|
# {"usernames": ("testuser1", "testuser2"), "soft": False},
|
||||||
)
|
# )
|
||||||
],
|
# ],
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
("users", "set"),
|
# ("users", "set"),
|
||||||
(
|
# (
|
||||||
"testuser1",
|
# "testuser1",
|
||||||
"testuser2",
|
# "testuser2",
|
||||||
"--no-input",
|
# "--no-input",
|
||||||
"--inactive",
|
# "--inactive",
|
||||||
"--upload-quota",
|
# "--upload-quota",
|
||||||
"35",
|
# "35",
|
||||||
"--no-staff",
|
# "--no-staff",
|
||||||
"--superuser",
|
# "--superuser",
|
||||||
"--permission-library",
|
# "--permission-library",
|
||||||
"--no-permission-moderation",
|
# "--no-permission-moderation",
|
||||||
"--no-permission-settings",
|
# "--no-permission-settings",
|
||||||
"--password",
|
# "--password",
|
||||||
"newpassword",
|
# "newpassword",
|
||||||
),
|
# ),
|
||||||
[
|
# [
|
||||||
(
|
# (
|
||||||
users,
|
# users,
|
||||||
"handler_update_user",
|
# "handler_update_user",
|
||||||
{
|
# {
|
||||||
"usernames": ("testuser1", "testuser2"),
|
# "usernames": ("testuser1", "testuser2"),
|
||||||
"kwargs": {
|
# "kwargs": {
|
||||||
"is_active": False,
|
# "is_active": False,
|
||||||
"upload_quota": 35,
|
# "upload_quota": 35,
|
||||||
"is_staff": False,
|
# "is_staff": False,
|
||||||
"is_superuser": True,
|
# "is_superuser": True,
|
||||||
"permission_library": True,
|
# "permission_library": True,
|
||||||
"permission_moderation": False,
|
# "permission_moderation": False,
|
||||||
"permission_settings": False,
|
# "permission_settings": False,
|
||||||
"password": "newpassword",
|
# "password": "newpassword",
|
||||||
},
|
# },
|
||||||
},
|
# },
|
||||||
)
|
# )
|
||||||
],
|
# ],
|
||||||
),
|
# ),
|
||||||
(
|
# (
|
||||||
("albums", "add-tags-from-tracks"),
|
# ("albums", "add-tags-from-tracks"),
|
||||||
tuple(),
|
# tuple(),
|
||||||
[(library, "handler_add_tags_from_tracks", {"albums": True})],
|
# [(library, "handler_add_tags_from_tracks", {"albums": True})],
|
||||||
),
|
# ),
|
||||||
(
|
(
|
||||||
("artists", "add-tags-from-tracks"),
|
("artists", "add-tags-from-tracks"),
|
||||||
tuple(),
|
tuple(),
|
||||||
|
|
|
@ -23,6 +23,13 @@ def test_load_test_data_dry_run(factories, mocker):
|
||||||
{"create_dependencies": True, "artists": 10},
|
{"create_dependencies": True, "artists": 10},
|
||||||
[(music_models.Artist.objects.all(), 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},
|
{"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.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):
|
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)
|
call_command("load_test_data", dry_run=False, albums=10, create_dependencies=False)
|
||||||
|
|
||||||
assert music_models.Artist.objects.count() == 5
|
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):
|
def test_attachment_queryset_attached(args, expected, factories, queryset_equal_list):
|
||||||
attachments = [
|
attachments = [
|
||||||
factories["music.Album"](
|
factories["music.Album"](
|
||||||
with_cover=True, artist__attachment_cover=None
|
with_cover=True, artist_credit__artist__attachment_cover=None
|
||||||
).attachment_cover,
|
).attachment_cover,
|
||||||
factories["common.Attachment"](),
|
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):
|
def test_track_favorite_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||||
factories["favorites.TrackFavorite"]()
|
factories["favorites.TrackFavorite"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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()
|
qs = models.TrackFavorite.objects.all()
|
||||||
filterset = filters.TrackFavoriteFilter(
|
filterset = filters.TrackFavoriteFilter(
|
||||||
{"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
|
{"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"]()
|
factories["favorites.TrackFavorite"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||||
hidden_fav = factories["favorites.TrackFavorite"](
|
hidden_fav = factories["favorites.TrackFavorite"](
|
||||||
track__album__artist=cf.target_artist
|
track__album__artist_credit__artist=cf.target_artist
|
||||||
)
|
)
|
||||||
qs = models.TrackFavorite.objects.all()
|
qs = models.TrackFavorite.objects.all()
|
||||||
filterset = filters.TrackFavoriteFilter(
|
filterset = filters.TrackFavoriteFilter(
|
||||||
|
|
|
@ -353,7 +353,7 @@ def test_inbox_create_audio(factories, mocker):
|
||||||
def test_inbox_create_audio_channel(factories, mocker):
|
def test_inbox_create_audio_channel(factories, mocker):
|
||||||
activity = factories["federation.Activity"]()
|
activity = factories["federation.Activity"]()
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
album = factories["music.Album"](artist=channel.artist)
|
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||||
upload = factories["music.Upload"](
|
upload = factories["music.Upload"](
|
||||||
track__album=album,
|
track__album=album,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
|
@ -423,7 +423,7 @@ def test_inbox_delete_album(factories):
|
||||||
|
|
||||||
def test_inbox_delete_album_channel(factories):
|
def test_inbox_delete_album_channel(factories):
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
album = factories["music.Album"](artist=channel.artist)
|
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||||
payload = {
|
payload = {
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"actor": channel.actor.fid,
|
"actor": channel.actor.fid,
|
||||||
|
@ -454,7 +454,7 @@ def test_outbox_delete_album(factories):
|
||||||
|
|
||||||
def test_outbox_delete_album_channel(factories):
|
def test_outbox_delete_album_channel(factories):
|
||||||
channel = factories["audio.Channel"]()
|
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]
|
a = list(routes.outbox_delete_album({"album": album}))[0]
|
||||||
expected = serializers.ActivitySerializer(
|
expected = serializers.ActivitySerializer(
|
||||||
{"type": "Delete", "object": {"type": "Album", "id": album.fid}}
|
{"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):
|
def test_inbox_delete_audio_channel(factories):
|
||||||
activity = factories["federation.Activity"]()
|
activity = factories["federation.Activity"]()
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
upload = factories["music.Upload"](track__artist=channel.artist)
|
upload = factories["music.Upload"](track__artist_credit__artist=channel.artist)
|
||||||
payload = {
|
payload = {
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
"actor": channel.actor.fid,
|
"actor": channel.actor.fid,
|
||||||
|
@ -816,7 +816,7 @@ def test_inbox_update_audio(factories, mocker, r_mock):
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
upload = factories["music.Upload"](
|
upload = factories["music.Upload"](
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
track__attributed_to=channel.actor,
|
track__attributed_to=channel.actor,
|
||||||
)
|
)
|
||||||
upload.track.fid = upload.fid
|
upload.track.fid = upload.fid
|
||||||
|
|
|
@ -787,11 +787,11 @@ def test_activity_pub_album_serializer_to_ap(factories):
|
||||||
"musicbrainzId": album.mbid,
|
"musicbrainzId": album.mbid,
|
||||||
"published": album.creation_date.isoformat(),
|
"published": album.creation_date.isoformat(),
|
||||||
"released": album.release_date.isoformat(),
|
"released": album.release_date.isoformat(),
|
||||||
"artists": [
|
"artist_credit": serializers.ArtistCreditSerializer(
|
||||||
serializers.ArtistSerializer(
|
album.artist_credit.all(),
|
||||||
album.artist, context={"include_ap_context": False}
|
many=True,
|
||||||
).data
|
context={"include_ap_context": False},
|
||||||
],
|
).data,
|
||||||
"attributedTo": album.attributed_to.fid,
|
"attributedTo": album.attributed_to.fid,
|
||||||
"mediaType": "text/html",
|
"mediaType": "text/html",
|
||||||
"content": common_utils.render_html(content.text, content.content_type),
|
"content": common_utils.render_html(content.text, content.content_type),
|
||||||
|
@ -808,19 +808,39 @@ def test_activity_pub_album_serializer_to_ap(factories):
|
||||||
def test_activity_pub_album_serializer_to_ap_channel_artist(factories):
|
def test_activity_pub_album_serializer_to_ap_channel_artist(factories):
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
album = factories["music.Album"](
|
album = factories["music.Album"](
|
||||||
artist=channel.artist,
|
artist_credit__artist=channel.artist,
|
||||||
)
|
)
|
||||||
|
|
||||||
serializer = serializers.AlbumSerializer(album)
|
serializer = serializers.AlbumSerializer(album)
|
||||||
|
assert serializer.data["artist_credit"] == [
|
||||||
assert serializer.data["artists"] == [
|
{
|
||||||
{"type": channel.actor.type, "id": channel.actor.fid}
|
"type": "ArtistCredit",
|
||||||
|
"id": album.artist_credit.all()[0].fid,
|
||||||
|
"artist": {
|
||||||
|
"type": "Artist",
|
||||||
|
"id": album.artist_credit.all()[0].artist.fid,
|
||||||
|
"name": album.artist_credit.all()[0].artist.name,
|
||||||
|
"published": album.artist_credit.all()[
|
||||||
|
0
|
||||||
|
].artist.creation_date.isoformat(),
|
||||||
|
"musicbrainzId": str(album.artist_credit.all()[0].artist.mbid),
|
||||||
|
"attributedTo": album.artist_credit.all()[0].artist.attributed_to.fid,
|
||||||
|
"tag": [],
|
||||||
|
"image": None,
|
||||||
|
},
|
||||||
|
"joinphrase": "",
|
||||||
|
"credit": album.artist_credit.all()[0].credit,
|
||||||
|
"index": None,
|
||||||
|
"published": album.artist_credit.all()[0].creation_date.isoformat(),
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
||||||
actor = factories["federation.Actor"]()
|
actor = factories["federation.Actor"]()
|
||||||
artist = factories["music.Artist"]()
|
artist = factories["music.Artist"]()
|
||||||
|
artist_credit = factories["music.ArtistCredit"](artist=artist)
|
||||||
|
|
||||||
released = faker.date_object()
|
released = faker.date_object()
|
||||||
payload = {
|
payload = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
|
@ -831,11 +851,9 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
||||||
"musicbrainzId": faker.uuid4(),
|
"musicbrainzId": faker.uuid4(),
|
||||||
"published": now.isoformat(),
|
"published": now.isoformat(),
|
||||||
"released": released.isoformat(),
|
"released": released.isoformat(),
|
||||||
"artists": [
|
"artist_credit": serializers.ArtistCreditSerializer(
|
||||||
serializers.ArtistSerializer(
|
[artist_credit], many=True, context={"include_ap_context": False}
|
||||||
artist, context={"include_ap_context": False}
|
).data,
|
||||||
).data
|
|
||||||
],
|
|
||||||
"attributedTo": actor.fid,
|
"attributedTo": actor.fid,
|
||||||
"tag": [
|
"tag": [
|
||||||
{"type": "Hashtag", "name": "#Punk"},
|
{"type": "Hashtag", "name": "#Punk"},
|
||||||
|
@ -850,7 +868,7 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
||||||
assert album.title == payload["name"]
|
assert album.title == payload["name"]
|
||||||
assert str(album.mbid) == payload["musicbrainzId"]
|
assert str(album.mbid) == payload["musicbrainzId"]
|
||||||
assert album.release_date == released
|
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.url == payload["image"]["href"]
|
||||||
assert album.attachment_cover.mimetype == payload["image"]["mediaType"]
|
assert album.attachment_cover.mimetype == payload["image"]["mediaType"]
|
||||||
assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
|
assert sorted(album.tagged_items.values_list("tag__name", flat=True)) == [
|
||||||
|
@ -860,10 +878,12 @@ def test_activity_pub_album_serializer_from_ap_create(factories, faker, now):
|
||||||
|
|
||||||
|
|
||||||
def test_activity_pub_album_serializer_from_ap_create_channel_artist(
|
def test_activity_pub_album_serializer_from_ap_create_channel_artist(
|
||||||
factories, faker, now
|
factories, faker, now, mocker
|
||||||
):
|
):
|
||||||
actor = factories["federation.Actor"]()
|
actor = factories["federation.Actor"]()
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
|
ac = factories["music.ArtistCredit"](artist=channel.artist)
|
||||||
|
|
||||||
released = faker.date_object()
|
released = faker.date_object()
|
||||||
payload = {
|
payload = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
|
@ -872,15 +892,40 @@ def test_activity_pub_album_serializer_from_ap_create_channel_artist(
|
||||||
"name": faker.sentence(),
|
"name": faker.sentence(),
|
||||||
"published": now.isoformat(),
|
"published": now.isoformat(),
|
||||||
"released": released.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,
|
"attributedTo": actor.fid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mocker.patch.object(utils, "retrieve_ap_object", return_value=ac)
|
||||||
serializer = serializers.AlbumSerializer(data=payload)
|
serializer = serializers.AlbumSerializer(data=payload)
|
||||||
assert serializer.is_valid(raise_exception=True) is True
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
album = serializer.save()
|
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):
|
def test_activity_pub_album_serializer_from_ap_update(factories, faker):
|
||||||
|
@ -895,11 +940,11 @@ def test_activity_pub_album_serializer_from_ap_update(factories, faker):
|
||||||
"musicbrainzId": faker.uuid4(),
|
"musicbrainzId": faker.uuid4(),
|
||||||
"published": album.creation_date.isoformat(),
|
"published": album.creation_date.isoformat(),
|
||||||
"released": released.isoformat(),
|
"released": released.isoformat(),
|
||||||
"artists": [
|
"artist_credit": serializers.ArtistCreditSerializer(
|
||||||
serializers.ArtistSerializer(
|
album.artist_credit.all(),
|
||||||
album.artist, context={"include_ap_context": False}
|
many=True,
|
||||||
).data
|
context={"include_ap_context": False},
|
||||||
],
|
).data,
|
||||||
"attributedTo": album.attributed_to.fid,
|
"attributedTo": album.attributed_to.fid,
|
||||||
"tag": [
|
"tag": [
|
||||||
{"type": "Hashtag", "name": "#Punk"},
|
{"type": "Hashtag", "name": "#Punk"},
|
||||||
|
@ -946,11 +991,11 @@ def test_activity_pub_track_serializer_to_ap(factories):
|
||||||
"disc": track.disc_number,
|
"disc": track.disc_number,
|
||||||
"license": track.license.conf["identifiers"][0],
|
"license": track.license.conf["identifiers"][0],
|
||||||
"copyright": "test",
|
"copyright": "test",
|
||||||
"artists": [
|
"artist_credit": serializers.ArtistCreditSerializer(
|
||||||
serializers.ArtistSerializer(
|
track.artist_credit.all(),
|
||||||
track.artist, context={"include_ap_context": False}
|
many=True,
|
||||||
).data
|
context={"include_ap_context": False},
|
||||||
],
|
).data,
|
||||||
"album": serializers.AlbumSerializer(
|
"album": serializers.AlbumSerializer(
|
||||||
track.album, context={"include_ap_context": False}
|
track.album, context={"include_ap_context": False}
|
||||||
).data,
|
).data,
|
||||||
|
@ -1014,41 +1059,53 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
"mediaType": "image/png",
|
"mediaType": "image/png",
|
||||||
},
|
},
|
||||||
"tag": [{"type": "Hashtag", "name": "AlbumTag"}],
|
"tag": [{"type": "Hashtag", "name": "AlbumTag"}],
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"type": "Artist",
|
"artist": {
|
||||||
"mediaType": "text/plain",
|
"type": "Artist",
|
||||||
"content": "Artist summary",
|
"mediaType": "text/plain",
|
||||||
"id": "http://hello.artist",
|
"content": "Artist summary",
|
||||||
"name": "John Smith",
|
"id": "http://hello.artist",
|
||||||
"musicbrainzId": str(uuid.uuid4()),
|
"name": "John Smith",
|
||||||
"published": published.isoformat(),
|
"musicbrainzId": str(uuid.uuid4()),
|
||||||
"attributedTo": album_artist_attributed_to.fid,
|
"published": published.isoformat(),
|
||||||
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
"attributedTo": album_artist_attributed_to.fid,
|
||||||
"image": {
|
"tag": [{"type": "Hashtag", "name": "AlbumArtistTag"}],
|
||||||
"type": "Image",
|
"image": {
|
||||||
"url": "https://cover.image/album-artist.png",
|
"type": "Image",
|
||||||
"mediaType": "image/png",
|
"url": "https://cover.image/album-artist.png",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
"joinphrase": "",
|
||||||
|
"credit": "John Smith Credit",
|
||||||
|
"published": published.isoformat(),
|
||||||
|
"id": "http://hello.artistcredit",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"type": "Artist",
|
"artist": {
|
||||||
"id": "http://hello.trackartist",
|
"type": "Artist",
|
||||||
"name": "Bob Smith",
|
"id": "http://hello.trackartist",
|
||||||
"mediaType": "text/plain",
|
"name": "Bob Smith",
|
||||||
"content": "Other artist summary",
|
"mediaType": "text/plain",
|
||||||
"musicbrainzId": str(uuid.uuid4()),
|
"content": "Other artist summary",
|
||||||
"attributedTo": artist_attributed_to.fid,
|
"musicbrainzId": str(uuid.uuid4()),
|
||||||
"published": published.isoformat(),
|
"attributedTo": artist_attributed_to.fid,
|
||||||
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
"published": published.isoformat(),
|
||||||
"image": {
|
"tag": [{"type": "Hashtag", "name": "ArtistTag"}],
|
||||||
"type": "Image",
|
"image": {
|
||||||
"url": "https://cover.image/artist.png",
|
"type": "Image",
|
||||||
"mediaType": "image/png",
|
"url": "https://cover.image/artist.png",
|
||||||
|
"mediaType": "image/png",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
"joinphrase": "",
|
||||||
|
"credit": "Credit Name",
|
||||||
|
"published": published.isoformat(),
|
||||||
|
"id": "http://hello.artistcredit",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"tag": [
|
"tag": [
|
||||||
|
@ -1061,8 +1118,8 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
|
|
||||||
track = serializer.save()
|
track = serializer.save()
|
||||||
album = track.album
|
album = track.album
|
||||||
artist = track.artist
|
artist = track.artist_credit.all()[0].artist
|
||||||
album_artist = track.album.artist
|
album_artist = track.album.artist_credit.all()[0].artist
|
||||||
|
|
||||||
assert track.from_activity == activity
|
assert track.from_activity == activity
|
||||||
assert track.fid == data["id"]
|
assert track.fid == data["id"]
|
||||||
|
@ -1090,33 +1147,55 @@ def test_activity_pub_track_serializer_from_ap(factories, r_mock, mocker):
|
||||||
assert album.description.content_type == data["album"]["mediaType"]
|
assert album.description.content_type == data["album"]["mediaType"]
|
||||||
|
|
||||||
assert artist.from_activity == activity
|
assert artist.from_activity == activity
|
||||||
assert artist.name == data["artists"][0]["name"]
|
assert artist.name == data["artist_credit"][0]["artist"]["name"]
|
||||||
assert artist.fid == data["artists"][0]["id"]
|
assert track.artist_credit.all()[0].credit == data["artist_credit"][0]["credit"]
|
||||||
assert str(artist.mbid) == data["artists"][0]["musicbrainzId"]
|
assert (
|
||||||
|
album.artist_credit.all()[0].credit
|
||||||
|
== data["album"]["artist_credit"][0]["credit"]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert artist.fid == data["artist_credit"][0]["artist"]["id"]
|
||||||
|
assert str(artist.mbid) == data["artist_credit"][0]["artist"]["musicbrainzId"]
|
||||||
assert artist.creation_date == published
|
assert artist.creation_date == published
|
||||||
assert artist.attributed_to == artist_attributed_to
|
assert artist.attributed_to == artist_attributed_to
|
||||||
assert artist.description.text == data["artists"][0]["content"]
|
assert artist.description.text == data["artist_credit"][0]["artist"]["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 (
|
assert (
|
||||||
album_artist.description.content_type
|
artist.description.content_type
|
||||||
== data["album"]["artists"][0]["mediaType"]
|
== data["artist_credit"][0]["artist"]["mediaType"]
|
||||||
)
|
)
|
||||||
assert (
|
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 (
|
assert (
|
||||||
album_artist.attachment_cover.mimetype
|
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"])
|
add_tags.assert_any_call(track, *["Hello", "World"])
|
||||||
|
@ -1144,7 +1223,9 @@ def test_activity_pub_track_serializer_from_ap_update(factories, r_mock, mocker,
|
||||||
"content": "hello there",
|
"content": "hello there",
|
||||||
"attributedTo": track_attributed_to.fid,
|
"attributedTo": track_attributed_to.fid,
|
||||||
"album": serializers.AlbumSerializer(track.album).data,
|
"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()},
|
"image": {"type": "Image", "mediaType": "image/jpeg", "url": faker.url()},
|
||||||
"tag": [
|
"tag": [
|
||||||
{"type": "Hashtag", "name": "#Hello"},
|
{"type": "Hashtag", "name": "#Hello"},
|
||||||
|
@ -1213,23 +1294,35 @@ def test_activity_pub_upload_serializer_from_ap(factories, mocker, r_mock):
|
||||||
"href": "https://cover.image/test.png",
|
"href": "https://cover.image/test.png",
|
||||||
"mediaType": "image/png",
|
"mediaType": "image/png",
|
||||||
},
|
},
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"type": "Artist",
|
"artist": {
|
||||||
"id": "http://hello.artist",
|
"type": "Artist",
|
||||||
"name": "John Smith",
|
"id": "http://hello.artist",
|
||||||
"musicbrainzId": str(uuid.uuid4()),
|
"name": "John Smith",
|
||||||
|
"musicbrainzId": str(uuid.uuid4()),
|
||||||
|
"published": published.isoformat(),
|
||||||
|
},
|
||||||
|
"joinphrase": "",
|
||||||
|
"credit": "John Smith Credit",
|
||||||
"published": published.isoformat(),
|
"published": published.isoformat(),
|
||||||
|
"id": "http://hello.artistcredit",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"type": "Artist",
|
"artist": {
|
||||||
"id": "http://hello.trackartist",
|
"type": "Artist",
|
||||||
"name": "Bob Smith",
|
"id": "http://hello.trackartist",
|
||||||
"musicbrainzId": str(uuid.uuid4()),
|
"name": "Bob Smith",
|
||||||
|
"musicbrainzId": str(uuid.uuid4()),
|
||||||
|
"published": published.isoformat(),
|
||||||
|
},
|
||||||
|
"joinphrase": "",
|
||||||
|
"credit": "Bob Smith Credit",
|
||||||
"published": published.isoformat(),
|
"published": published.isoformat(),
|
||||||
|
"id": "http://hello.artistcredit",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1259,7 +1352,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):
|
def test_activity_pub_upload_serializer_from_ap_update(factories, mocker, now, r_mock):
|
||||||
library = factories["music.Library"]()
|
library = factories["music.Library"]()
|
||||||
upload = factories["music.Upload"](library=library, track__album__with_cover=True)
|
upload = factories["music.Upload"](library=library, track__album__with_cover=True)
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"type": "Audio",
|
"type": "Audio",
|
||||||
|
@ -1631,7 +1723,7 @@ def test_channel_actor_outbox_serializer(factories):
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
uploads = factories["music.Upload"].create_batch(
|
uploads = factories["music.Upload"].create_batch(
|
||||||
5,
|
5,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
import_status="finished",
|
import_status="finished",
|
||||||
)
|
)
|
||||||
|
@ -1671,7 +1763,8 @@ def test_channel_upload_serializer(factories):
|
||||||
track__license="cc0-1.0",
|
track__license="cc0-1.0",
|
||||||
track__copyright="Copyright something",
|
track__copyright="Copyright something",
|
||||||
track__album__set_tags=["Rock"],
|
track__album__set_tags=["Rock"],
|
||||||
track__artist__set_tags=["Indie"],
|
track__artist_credit__artist__set_tags=["Indie"],
|
||||||
|
track__artist_credit__artist__local=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -1722,9 +1815,9 @@ def test_channel_upload_serializer(factories):
|
||||||
assert serializer.data == expected
|
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")
|
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 = {
|
payload = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"type": "Audio",
|
"type": "Audio",
|
||||||
|
@ -1767,6 +1860,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
||||||
"url": "https://image.example/image.png",
|
"url": "https://image.example/image.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
mocker.patch.object(utils, "retrieve_ap_object", return_value=album)
|
||||||
|
|
||||||
serializer = serializers.ChannelUploadSerializer(
|
serializer = serializers.ChannelUploadSerializer(
|
||||||
data=payload, context={"channel": channel}
|
data=payload, context={"channel": channel}
|
||||||
|
@ -1784,7 +1878,7 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
||||||
assert upload.size == payload["url"][1]["size"]
|
assert upload.size == payload["url"][1]["size"]
|
||||||
assert upload.bitrate == payload["url"][1]["bitrate"]
|
assert upload.bitrate == payload["url"][1]["bitrate"]
|
||||||
assert upload.duration == payload["duration"]
|
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.position == payload["position"]
|
||||||
assert upload.track.disc_number == payload["disc"]
|
assert upload.track.disc_number == payload["disc"]
|
||||||
assert upload.track.attributed_to == channel.attributed_to
|
assert upload.track.attributed_to == channel.attributed_to
|
||||||
|
@ -1801,10 +1895,12 @@ def test_channel_upload_serializer_from_ap_create(factories, now):
|
||||||
assert upload.track.album == album
|
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")
|
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||||
album = factories["music.Album"](artist=channel.artist)
|
album = factories["music.Album"](artist_credit__artist=channel.artist)
|
||||||
upload = factories["music.Upload"](track__album=album, track__artist=channel.artist)
|
upload = factories["music.Upload"](
|
||||||
|
track__album=album, track__artist_credit__artist=channel.artist
|
||||||
|
)
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
|
@ -1847,6 +1943,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now):
|
||||||
"url": "https://image.example/image.png",
|
"url": "https://image.example/image.png",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
mocker.patch.object(utils, "retrieve_ap_object", return_value=album)
|
||||||
|
|
||||||
serializer = serializers.ChannelUploadSerializer(
|
serializer = serializers.ChannelUploadSerializer(
|
||||||
data=payload, context={"channel": channel}
|
data=payload, context={"channel": channel}
|
||||||
|
@ -1865,7 +1962,7 @@ def test_channel_upload_serializer_from_ap_update(factories, now):
|
||||||
assert upload.size == payload["url"][1]["size"]
|
assert upload.size == payload["url"][1]["size"]
|
||||||
assert upload.bitrate == payload["url"][1]["bitrate"]
|
assert upload.bitrate == payload["url"][1]["bitrate"]
|
||||||
assert upload.duration == payload["duration"]
|
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.position == payload["position"]
|
||||||
assert upload.track.disc_number == payload["disc"]
|
assert upload.track.disc_number == payload["disc"]
|
||||||
assert upload.track.attributed_to == channel.attributed_to
|
assert upload.track.attributed_to == channel.attributed_to
|
||||||
|
@ -1944,3 +2041,23 @@ def test_report_serializer_to_ap(factories):
|
||||||
}
|
}
|
||||||
serializer = serializers.FlagSerializer(report)
|
serializer = serializers.FlagSerializer(report)
|
||||||
assert serializer.data == expected
|
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,
|
||||||
|
"credit": ac.credit,
|
||||||
|
"index": ac.index,
|
||||||
|
"published": ac.creation_date.isoformat(),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
|
@ -4,7 +4,9 @@ from funkwhale_api.history import filters, models
|
||||||
def test_listening_filter_track_artist(factories, mocker, queryset_equal_list):
|
def test_listening_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||||
factories["history.Listening"]()
|
factories["history.Listening"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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()
|
qs = models.Listening.objects.all()
|
||||||
filterset = filters.ListeningFilter(
|
filterset = filters.ListeningFilter(
|
||||||
{"hidden": "true"}, request=mocker.Mock(user=cf.user), queryset=qs
|
{"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"]()
|
factories["history.Listening"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
cf = factories["moderation.UserFilter"](for_artist=True)
|
||||||
hidden_listening = factories["history.Listening"](
|
hidden_listening = factories["history.Listening"](
|
||||||
track__album__artist=cf.target_artist
|
track__album__artist_credit__artist=cf.target_artist
|
||||||
)
|
)
|
||||||
qs = models.Listening.objects.all()
|
qs = models.Listening.objects.all()
|
||||||
filterset = filters.ListeningFilter(
|
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),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"release_date": album.release_date.isoformat(),
|
"release_date": album.release_date.isoformat(),
|
||||||
"cover": common_serializers.AttachmentSerializer(album.attachment_cover).data,
|
"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(
|
"attributed_to": serializers.ManageBaseActorSerializer(
|
||||||
album.attributed_to
|
album.attributed_to
|
||||||
).data,
|
).data,
|
||||||
|
@ -412,7 +418,13 @@ def test_manage_track_serializer(factories, now, to_api_date):
|
||||||
"copyright": track.copyright,
|
"copyright": track.copyright,
|
||||||
"license": track.license,
|
"license": track.license,
|
||||||
"creation_date": to_api_date(track.creation_date),
|
"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,
|
"album": serializers.ManageTrackAlbumSerializer(track.album).data,
|
||||||
"attributed_to": serializers.ManageBaseActorSerializer(
|
"attributed_to": serializers.ManageBaseActorSerializer(
|
||||||
track.attributed_to
|
track.attributed_to
|
||||||
|
|
|
@ -216,7 +216,9 @@ def test_album_list(factories, superuser_api_client, settings):
|
||||||
album = factories["music.Album"]()
|
album = factories["music.Album"]()
|
||||||
factories["music.Album"]()
|
factories["music.Album"]()
|
||||||
url = reverse("api:v1:manage:library:albums-list")
|
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
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
|
@ -33,7 +33,7 @@ def test_album_filter_hidden(factories, mocker, queryset_equal_list):
|
||||||
factories["music.Album"]()
|
factories["music.Album"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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()
|
qs = models.Album.objects.all()
|
||||||
filterset = filters.AlbumFilter(
|
filterset = filters.AlbumFilter(
|
||||||
|
@ -59,7 +59,7 @@ def test_artist_filter_hidden(factories, mocker, queryset_equal_list):
|
||||||
def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
|
def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||||
factories["music.Track"]()
|
factories["music.Track"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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()
|
qs = models.Track.objects.all()
|
||||||
filterset = filters.TrackFilter(
|
filterset = filters.TrackFilter(
|
||||||
|
@ -72,7 +72,9 @@ def test_artist_filter_track_artist(factories, mocker, queryset_equal_list):
|
||||||
def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list):
|
def test_artist_filter_track_album_artist(factories, mocker, queryset_equal_list):
|
||||||
factories["music.Track"]()
|
factories["music.Track"]()
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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()
|
qs = models.Track.objects.all()
|
||||||
filterset = filters.TrackFilter(
|
filterset = filters.TrackFilter(
|
||||||
|
@ -141,7 +143,9 @@ def test_track_filter_tag_multiple(
|
||||||
def test_channel_filter_track(factories, queryset_equal_list, mocker, anonymous_user):
|
def test_channel_filter_track(factories, queryset_equal_list, mocker, anonymous_user):
|
||||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||||
upload = factories["music.Upload"](
|
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"]()
|
factories["music.Track"]()
|
||||||
qs = upload.track.__class__.objects.all()
|
qs = upload.track.__class__.objects.all()
|
||||||
|
@ -157,7 +161,9 @@ def test_channel_filter_track(factories, queryset_equal_list, mocker, anonymous_
|
||||||
def test_channel_filter_album(factories, queryset_equal_list, mocker, anonymous_user):
|
def test_channel_filter_album(factories, queryset_equal_list, mocker, anonymous_user):
|
||||||
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
channel = factories["audio.Channel"](library__privacy_level="everyone")
|
||||||
upload = factories["music.Upload"](
|
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"]()
|
factories["music.Album"]()
|
||||||
qs = upload.track.album.__class__.objects.all()
|
qs = upload.track.album.__class__.objects.all()
|
||||||
|
@ -202,14 +208,14 @@ def test_library_filter_artist(factories, queryset_equal_list, mocker, anonymous
|
||||||
library = factories["music.Library"](privacy_level="everyone")
|
library = factories["music.Library"](privacy_level="everyone")
|
||||||
upload = factories["music.Upload"](library=library, playable=True)
|
upload = factories["music.Upload"](library=library, playable=True)
|
||||||
factories["music.Artist"]()
|
factories["music.Artist"]()
|
||||||
qs = upload.track.artist.__class__.objects.all()
|
|
||||||
|
qs = models.Artist.objects.all()
|
||||||
filterset = filters.ArtistFilter(
|
filterset = filters.ArtistFilter(
|
||||||
{"library": library.uuid},
|
{"library": library.uuid},
|
||||||
request=mocker.Mock(user=anonymous_user, actor=None),
|
request=mocker.Mock(user=anonymous_user, actor=None),
|
||||||
queryset=qs,
|
queryset=qs,
|
||||||
)
|
)
|
||||||
|
assert filterset.qs == [upload.track.artist_credit.all()[0].artist]
|
||||||
assert filterset.qs == [upload.track.artist]
|
|
||||||
|
|
||||||
|
|
||||||
def test_track_filter_artist_includes_album_artist(
|
def test_track_filter_artist_includes_album_artist(
|
||||||
|
@ -218,12 +224,13 @@ def test_track_filter_artist_includes_album_artist(
|
||||||
factories["music.Track"]()
|
factories["music.Track"]()
|
||||||
track1 = factories["music.Track"]()
|
track1 = factories["music.Track"]()
|
||||||
track2 = 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()
|
qs = models.Track.objects.all()
|
||||||
filterset = filters.TrackFilter(
|
filterset = filters.TrackFilter(
|
||||||
{"artist": track1.artist.pk},
|
{"artist": track1.artist_credit.all()[0].artist.pk},
|
||||||
request=mocker.Mock(user=anonymous_user),
|
request=mocker.Mock(user=anonymous_user),
|
||||||
queryset=qs,
|
queryset=qs,
|
||||||
)
|
)
|
||||||
|
|
|
@ -312,23 +312,29 @@ def test_metadata_fallback_ogg_theora(mocker):
|
||||||
"test.mp3",
|
"test.mp3",
|
||||||
{
|
{
|
||||||
"title": "Bend",
|
"title": "Bend",
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Binärpilot",
|
"credit": "Binärpilot",
|
||||||
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
||||||
|
"joinphrase": "; ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credit": "Another artist",
|
||||||
|
"mbid": None,
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
{"name": "Another artist", "mbid": None},
|
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "You Can't Stop Da Funk",
|
"title": "You Can't Stop Da Funk",
|
||||||
"mbid": uuid.UUID("ce40cdb1-a562-4fd8-a269-9269f98d4124"),
|
"mbid": uuid.UUID("ce40cdb1-a562-4fd8-a269-9269f98d4124"),
|
||||||
"release_date": datetime.date(2006, 2, 7),
|
"release_date": datetime.date(2006, 2, 7),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Binärpilot",
|
"credit": "Binärpilot",
|
||||||
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
"mbid": uuid.UUID("9c6bddde-6228-4d9f-ad0d-03f6fcb19e13"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{"name": "Another artist", "mbid": None},
|
{"credit": "Another artist", "mbid": None, "joinphrase": ""},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
"position": 2,
|
"position": 2,
|
||||||
|
@ -344,25 +350,32 @@ def test_metadata_fallback_ogg_theora(mocker):
|
||||||
"test.ogg",
|
"test.ogg",
|
||||||
{
|
{
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "; ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credit": "Musopen Symphony Orchestra",
|
||||||
|
"mbid": None,
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
{"name": "Musopen Symphony Orchestra", "mbid": None},
|
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||||
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
||||||
"release_date": datetime.date(2012, 8, 15),
|
"release_date": datetime.date(2012, 8, 15),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Musopen Symphony Orchestra",
|
"credit": "Musopen Symphony Orchestra",
|
||||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -379,25 +392,32 @@ def test_metadata_fallback_ogg_theora(mocker):
|
||||||
"test.opus",
|
"test.opus",
|
||||||
{
|
{
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "; ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credit": "Musopen Symphony Orchestra",
|
||||||
|
"mbid": None,
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
{"name": "Musopen Symphony Orchestra", "mbid": None},
|
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||||
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
||||||
"release_date": datetime.date(2012, 8, 15),
|
"release_date": datetime.date(2012, 8, 15),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Musopen Symphony Orchestra",
|
"credit": "Musopen Symphony Orchestra",
|
||||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -414,20 +434,22 @@ def test_metadata_fallback_ogg_theora(mocker):
|
||||||
"test_theora.ogg",
|
"test_theora.ogg",
|
||||||
{
|
{
|
||||||
"title": "Drei Kreuze (dass wir hier sind)",
|
"title": "Drei Kreuze (dass wir hier sind)",
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Die Toten Hosen",
|
"credit": "Die Toten Hosen",
|
||||||
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
||||||
|
"joinphrase": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Ballast der Republik",
|
"title": "Ballast der Republik",
|
||||||
"mbid": uuid.UUID("1f0441ad-e609-446d-b355-809c445773cf"),
|
"mbid": uuid.UUID("1f0441ad-e609-446d-b355-809c445773cf"),
|
||||||
"release_date": datetime.date(2012, 5, 4),
|
"release_date": datetime.date(2012, 5, 4),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Die Toten Hosen",
|
"credit": "Die Toten Hosen",
|
||||||
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
"mbid": uuid.UUID("c3bc80a6-1f4a-4e17-8cf0-6b1efe8302f1"),
|
||||||
|
"joinphrase": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -446,20 +468,22 @@ def test_metadata_fallback_ogg_theora(mocker):
|
||||||
"sample.flac",
|
"sample.flac",
|
||||||
{
|
{
|
||||||
"title": "999,999",
|
"title": "999,999",
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Nine Inch Nails",
|
"credit": "Nine Inch Nails",
|
||||||
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
||||||
|
"joinphrase": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "The Slip",
|
"title": "The Slip",
|
||||||
"mbid": uuid.UUID("12b57d46-a192-499e-a91f-7da66790a1c1"),
|
"mbid": uuid.UUID("12b57d46-a192-499e-a91f-7da66790a1c1"),
|
||||||
"release_date": datetime.date(2008, 5, 5),
|
"release_date": datetime.date(2008, 5, 5),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Nine Inch Nails",
|
"credit": "Nine Inch Nails",
|
||||||
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
"mbid": uuid.UUID("b7ffd2af-418f-4be2-bdd1-22f8b48613da"),
|
||||||
|
"joinphrase": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -497,12 +521,14 @@ def test_track_metadata_serializer(path, expected, mocker):
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Hello",
|
"credit": "Hello",
|
||||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "World",
|
"credit": "World",
|
||||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -513,14 +539,16 @@ def test_track_metadata_serializer(path, expected, mocker):
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"name": "Hello",
|
"credit": "Hello",
|
||||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcb"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "World",
|
"credit": "World",
|
||||||
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
"mbid": uuid.UUID("f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{"name": "Foo", "mbid": None},
|
{"credit": "Foo", "mbid": None, "joinphrase": ""},
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -533,8 +561,8 @@ def test_artists_cleaning(raw, expected):
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"data, errored_field",
|
"data, errored_field",
|
||||||
[
|
[
|
||||||
({"name": "Hello", "mbid": "wrong-uuid"}, "mbid"), # wrong uuid
|
({"credit": "Hello", "mbid": "wrong-uuid"}, "mbid"), # wrong uuid
|
||||||
({"name": "", "mbid": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"}, "name"),
|
({"credit": "", "mbid": "f269d497-1cc0-4ae4-a0c4-157ec7d73fcd"}, "credit"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_artist_serializer_validation(data, errored_field):
|
def test_artist_serializer_validation(data, errored_field):
|
||||||
|
@ -584,24 +612,27 @@ def test_fake_metadata_with_serializer():
|
||||||
expected = {
|
expected = {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||||
"description": {"text": "hello there", "content_type": "text/plain"},
|
"description": {"text": "hello there", "content_type": "text/plain"},
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "",
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||||
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
"mbid": uuid.UUID("a766da8b-8336-47aa-a3ee-371cc41ccc75"),
|
||||||
"release_date": datetime.date(2012, 8, 15),
|
"release_date": datetime.date(2012, 8, 15),
|
||||||
"artists": [
|
"artist_credit": [
|
||||||
{
|
{
|
||||||
"name": "Edvard Grieg",
|
"credit": "Edvard Grieg",
|
||||||
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
"mbid": uuid.UUID("013c8e5b-d72a-4cd3-8dee-6c64d6125823"),
|
||||||
|
"joinphrase": "; ",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Musopen Symphony Orchestra",
|
"credit": "Musopen Symphony Orchestra",
|
||||||
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
"mbid": uuid.UUID("5b4d7d2d-36df-4b38-95e3-a964234f520f"),
|
||||||
|
"joinphrase": "",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
|
@ -614,6 +645,7 @@ def test_fake_metadata_with_serializer():
|
||||||
}
|
}
|
||||||
serializer = metadata.TrackMetadataSerializer(data=metadata.FakeMetadata(data))
|
serializer = metadata.TrackMetadataSerializer(data=metadata.FakeMetadata(data))
|
||||||
assert serializer.is_valid(raise_exception=True) is True
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
|
|
||||||
assert serializer.validated_data == expected
|
assert serializer.validated_data == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -626,12 +658,12 @@ def test_serializer_album_artist_missing():
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
"title": "Peer Gynt Suite no. 1, op. 46: I. Morning",
|
||||||
"artists": [{"name": "Edvard Grieg", "mbid": None}],
|
"artist_credit": [{"credit": "Edvard Grieg", "mbid": None, "joinphrase": ""}],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Peer Gynt Suite no. 1, op. 46",
|
"title": "Peer Gynt Suite no. 1, op. 46",
|
||||||
"mbid": None,
|
"mbid": None,
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"artists": [],
|
"artist_credit": [],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -654,12 +686,12 @@ def test_serializer_album_artist_missing():
|
||||||
def test_serializer_album_default_title_when_missing_or_empty(data):
|
def test_serializer_album_default_title_when_missing_or_empty(data):
|
||||||
expected = {
|
expected = {
|
||||||
"title": "Track",
|
"title": "Track",
|
||||||
"artists": [{"name": "Artist", "mbid": None}],
|
"artist_credit": [{"credit": "Artist", "mbid": None, "joinphrase": ""}],
|
||||||
"album": {
|
"album": {
|
||||||
"title": metadata.UNKNOWN_ALBUM,
|
"title": metadata.UNKNOWN_ALBUM,
|
||||||
"mbid": None,
|
"mbid": None,
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"artists": [],
|
"artist_credit": [],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -681,12 +713,12 @@ def test_serializer_empty_fields(field_name):
|
||||||
}
|
}
|
||||||
expected = {
|
expected = {
|
||||||
"title": "Track Title",
|
"title": "Track Title",
|
||||||
"artists": [{"name": "Track Artist", "mbid": None}],
|
"artist_credit": [{"credit": "Track Artist", "mbid": None, "joinphrase": ""}],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Track Album",
|
"title": "Track Album",
|
||||||
"mbid": None,
|
"mbid": None,
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"artists": [],
|
"artist_credit": [],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -698,12 +730,12 @@ def test_serializer_empty_fields(field_name):
|
||||||
def test_serializer_strict_mode_false():
|
def test_serializer_strict_mode_false():
|
||||||
data = {}
|
data = {}
|
||||||
expected = {
|
expected = {
|
||||||
"artists": [{"name": None, "mbid": None}],
|
"artist_credit": [],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "[Unknown Album]",
|
"title": "[Unknown Album]",
|
||||||
"mbid": None,
|
"mbid": None,
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"artists": [],
|
"artist_credit": [],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -730,11 +762,21 @@ def test_artist_field_featuring():
|
||||||
"musicbrainz_artistid": "9a3bf45c-347d-4630-894d-7cf3e8e0b632/cbf9738d-8f81-4a92-bc64-ede09341652d",
|
"musicbrainz_artistid": "9a3bf45c-347d-4630-894d-7cf3e8e0b632/cbf9738d-8f81-4a92-bc64-ede09341652d",
|
||||||
}
|
}
|
||||||
|
|
||||||
expected = [{"name": "Santana feat. Chris Cornell", "mbid": None}]
|
expected = [
|
||||||
|
{
|
||||||
|
"credit": "Santana",
|
||||||
|
"mbid": uuid.UUID("9a3bf45c-347d-4630-894d-7cf3e8e0b632"),
|
||||||
|
"joinphrase": " feat. ",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"credit": "Chris Cornell",
|
||||||
|
"mbid": uuid.UUID("cbf9738d-8f81-4a92-bc64-ede09341652d"),
|
||||||
|
"joinphrase": "",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
field = metadata.ArtistField()
|
field = metadata.ArtistField()
|
||||||
value = field.get_value(data)
|
value = field.get_value(data)
|
||||||
|
|
||||||
assert field.to_internal_value(value) == expected
|
assert field.to_internal_value(value) == expected
|
||||||
|
|
||||||
|
|
||||||
|
@ -756,23 +798,23 @@ def test_acquire_tags_from_genre(genre, expected_tags):
|
||||||
data = {
|
data = {
|
||||||
"title": "Track Title",
|
"title": "Track Title",
|
||||||
"artist": "Track Artist",
|
"artist": "Track Artist",
|
||||||
|
# "artist_credit": [{"credit": "Track Artist"}],
|
||||||
"album": "Track Album",
|
"album": "Track Album",
|
||||||
"genre": genre,
|
"genre": genre,
|
||||||
}
|
}
|
||||||
expected = {
|
expected = {
|
||||||
"title": "Track Title",
|
"title": "Track Title",
|
||||||
"artists": [{"name": "Track Artist", "mbid": None}],
|
"artist_credit": [{"credit": "Track Artist", "mbid": None, "joinphrase": ""}],
|
||||||
"album": {
|
"album": {
|
||||||
"title": "Track Album",
|
"title": "Track Album",
|
||||||
"mbid": None,
|
"mbid": None,
|
||||||
"release_date": None,
|
"release_date": None,
|
||||||
"artists": [],
|
"artist_credit": [],
|
||||||
"cover_data": None,
|
"cover_data": None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if expected_tags:
|
if expected_tags:
|
||||||
expected["tags"] = expected_tags
|
expected["tags"] = expected_tags
|
||||||
|
|
||||||
serializer = metadata.TrackMetadataSerializer(data=metadata.FakeMetadata(data))
|
serializer = metadata.TrackMetadataSerializer(data=metadata.FakeMetadata(data))
|
||||||
assert serializer.is_valid(raise_exception=True) is True
|
assert serializer.is_valid(raise_exception=True) is True
|
||||||
assert serializer.validated_data == expected
|
assert serializer.validated_data == expected
|
||||||
|
|
|
@ -27,6 +27,48 @@
|
||||||
|
|
||||||
# upload_manager = new_apps.get_model(a, "Upload")
|
# upload_manager = new_apps.get_model(a, "Upload")
|
||||||
|
|
||||||
|
|
||||||
# for upload in mapping_list:
|
# for upload in mapping_list:
|
||||||
# upload_obj = upload_manager.objects.get(pk=upload[4])
|
# upload_obj = upload_manager.objects.get(pk=upload[4])
|
||||||
# assert upload_obj.quality == upload[3]
|
# assert upload_obj.quality == upload[3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_artist_credit_migration(migrator):
|
||||||
|
mapping_list = [("artist_name", "album_title", "track_title")]
|
||||||
|
|
||||||
|
a, f, t = (
|
||||||
|
"music",
|
||||||
|
"0058_upload_quality",
|
||||||
|
"0059_remove_album_artist_remove_track_artist_artistcredit_and_more",
|
||||||
|
)
|
||||||
|
|
||||||
|
migrator.migrate([(a, f)])
|
||||||
|
old_apps = migrator.loader.project_state([(a, f)]).apps
|
||||||
|
Track = old_apps.get_model(a, "Track")
|
||||||
|
Album = old_apps.get_model(a, "Album")
|
||||||
|
Artist = old_apps.get_model(a, "Artist")
|
||||||
|
|
||||||
|
for track in mapping_list:
|
||||||
|
artist = Artist.objects.create(name=track[0])
|
||||||
|
old_album = Album.objects.create(title=track[1], artist=artist)
|
||||||
|
old_track = Track.objects.create(title=track[2], artist=artist, album=old_album)
|
||||||
|
|
||||||
|
migrator.loader.build_graph()
|
||||||
|
|
||||||
|
migrator.migrate([(a, t)])
|
||||||
|
new_apps = migrator.loader.project_state([(a, t)]).apps
|
||||||
|
|
||||||
|
track_manager = new_apps.get_model(a, "Track")
|
||||||
|
album_manager = new_apps.get_model(a, "Album")
|
||||||
|
|
||||||
|
for track in mapping_list:
|
||||||
|
track_obj = track_manager.objects.get(title=track[2])
|
||||||
|
album_obj = album_manager.objects.get(title=track[1])
|
||||||
|
|
||||||
|
assert track_obj.artist_credit.all()[0].artist.pk == old_track.artist.pk
|
||||||
|
assert track_obj.artist_credit.all()[0].joinphrase == ""
|
||||||
|
assert track_obj.artist_credit.all()[0].credit == old_track.artist.name
|
||||||
|
|
||||||
|
assert album_obj.artist_credit.all()[0].artist.pk == old_album.artist.pk
|
||||||
|
assert album_obj.artist_credit.all()[0].joinphrase == ""
|
||||||
|
assert album_obj.artist_credit.all()[0].credit == old_album.artist.name
|
||||||
|
|
|
@ -43,7 +43,7 @@ def test_import_album_stores_release_group(factories):
|
||||||
album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[])
|
album = importers.load(models.Album, cleaned_data, album_data, import_hooks=[])
|
||||||
|
|
||||||
assert album.release_group_id == album_data["release-group"]["id"]
|
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):
|
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)
|
mocked_get.assert_called_once_with(album.mbid, includes=models.Album.api_includes)
|
||||||
assert track.title == track_data["recording"]["title"]
|
assert track.title == track_data["recording"]["title"]
|
||||||
assert track.mbid == track_data["recording"]["id"]
|
assert track.mbid == track_data["recording"]["id"]
|
||||||
assert track.album == album
|
assert track.artist_credit.all()[0].albums.all()[0] == album
|
||||||
assert track.artist == artist
|
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"])
|
assert track.position == int(track_data["position"])
|
||||||
|
|
||||||
|
|
||||||
|
@ -144,12 +208,11 @@ def test_import_track_with_different_artist_than_release(factories, mocker):
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
"funkwhale_api.musicbrainz.api.recordings.get", return_value=recording_data
|
"funkwhale_api.musicbrainz.api.recordings.get", return_value=recording_data
|
||||||
)
|
)
|
||||||
|
|
||||||
track = models.Track.get_or_create_from_api(recording_data["recording"]["id"])[0]
|
track = models.Track.get_or_create_from_api(recording_data["recording"]["id"])[0]
|
||||||
assert track.title == recording_data["recording"]["title"]
|
assert track.title == recording_data["recording"]["title"]
|
||||||
assert track.mbid == recording_data["recording"]["id"]
|
assert track.mbid == recording_data["recording"]["id"]
|
||||||
assert track.album == album
|
assert track.album == album
|
||||||
assert track.artist == artist
|
assert track.artist_credit.all()[0].artist == artist
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
|
@ -391,7 +454,7 @@ def test_artist_playable_by_correct_actor(privacy_level, expected, factories):
|
||||||
queryset = models.Artist.objects.playable_by(
|
queryset = models.Artist.objects.playable_by(
|
||||||
upload.library.actor
|
upload.library.actor
|
||||||
).annotate_playable_by_actor(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
|
assert match is expected
|
||||||
if expected:
|
if expected:
|
||||||
assert bool(queryset.first().is_playable_by_actor) is 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(
|
queryset = models.Artist.objects.playable_by(
|
||||||
instance_actor
|
instance_actor
|
||||||
).annotate_playable_by_actor(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
|
assert match is expected
|
||||||
if expected:
|
if expected:
|
||||||
assert bool(queryset.first().is_playable_by_actor) is 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,
|
library__local=True,
|
||||||
)
|
)
|
||||||
queryset = models.Artist.objects.playable_by(None).annotate_playable_by_actor(None)
|
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
|
assert match is expected
|
||||||
if expected:
|
if expected:
|
||||||
assert bool(queryset.first().is_playable_by_actor) is 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.mbid, data["id"]
|
||||||
assert album.title, "Hypnotize"
|
assert album.title, "Hypnotize"
|
||||||
assert album.release_date, datetime.date(2005, 1, 1)
|
assert album.release_date, datetime.date(2005, 1, 1)
|
||||||
assert album.artist.name, "System of a Down"
|
assert album.artist_credit.all()[0].artist.name, "System of a Down"
|
||||||
assert album.artist.mbid, data["artist-credit"][0]["artist"]["id"]
|
assert album.artist_credit.all()[0].artist.mbid, data["artist-credit"][0]["artist"][
|
||||||
|
"id"
|
||||||
|
]
|
||||||
assert album.fid == federation_utils.full_url(
|
assert album.fid == federation_utils.full_url(
|
||||||
f"/federation/music/albums/{album.uuid}"
|
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 int(data["ext:score"]) == 100
|
||||||
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
||||||
assert track.mbid == data["id"]
|
assert track.mbid == data["id"]
|
||||||
assert track.artist.pk is not None
|
assert track.artist_credit.all()[0].artist.pk is not None
|
||||||
assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
|
assert (
|
||||||
assert track.artist.name == "Adhesive Wombat"
|
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 str(track.album.mbid) == "a50d2a81-2a50-484d-9cb4-b9f6833f583e"
|
||||||
assert track.album.title == "Marsupial Madness"
|
assert track.album.title == "Marsupial Madness"
|
||||||
assert track.fid == federation_utils.full_url(
|
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 int(data["ext:score"]) == 100
|
||||||
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
assert data["id"] == "9968a9d6-8d92-4051-8f76-674e157b6eed"
|
||||||
assert track.mbid == data["id"]
|
assert track.mbid == data["id"]
|
||||||
assert track.artist.pk is not None
|
assert track.artist_credit.all()[0].artist.pk is not None
|
||||||
assert str(track.artist.mbid) == "62c3befb-6366-4585-b256-809472333801"
|
assert (
|
||||||
assert track.artist.name == "Adhesive Wombat"
|
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"])
|
track2, created = models.Track.get_or_create_from_api(mbid=data["id"])
|
||||||
assert not created
|
assert not created
|
||||||
|
|
|
@ -38,7 +38,7 @@ def test_artist_album_serializer(factories, to_api_date):
|
||||||
"fid": album.fid,
|
"fid": album.fid,
|
||||||
"mbid": str(album.mbid),
|
"mbid": str(album.mbid),
|
||||||
"title": album.title,
|
"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),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"tracks_count": 1,
|
"tracks_count": 1,
|
||||||
"is_playable": None,
|
"is_playable": None,
|
||||||
|
@ -53,13 +53,16 @@ def test_artist_album_serializer(factories, to_api_date):
|
||||||
|
|
||||||
def test_artist_with_albums_serializer(factories, to_api_date):
|
def test_artist_with_albums_serializer(factories, to_api_date):
|
||||||
actor = factories["federation.Actor"]()
|
actor = factories["federation.Actor"]()
|
||||||
track = factories["music.Track"](
|
artist_credit = factories["music.ArtistCredit"](
|
||||||
album__artist__attributed_to=actor, album__artist__with_cover=True
|
artist__attributed_to=actor, artist__with_cover=True
|
||||||
)
|
)
|
||||||
artist = track.artist
|
|
||||||
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
track = factories["music.Track"](
|
||||||
album = list(artist.albums.all())[0]
|
album__artist_credit=artist_credit, artist_credit=artist_credit
|
||||||
setattr(artist, "_prefetched_tracks", range(42))
|
)
|
||||||
|
artist = track.artist_credit.all()[0].artist
|
||||||
|
album = artist.artist_credit.all()[0].albums.all()[0]
|
||||||
|
setattr(artist, "_tracks_count", 42)
|
||||||
expected = {
|
expected = {
|
||||||
"id": artist.id,
|
"id": artist.id,
|
||||||
"fid": artist.fid,
|
"fid": artist.fid,
|
||||||
|
@ -81,12 +84,16 @@ def test_artist_with_albums_serializer(factories, to_api_date):
|
||||||
|
|
||||||
def test_artist_with_albums_serializer_channel(factories, to_api_date):
|
def test_artist_with_albums_serializer_channel(factories, to_api_date):
|
||||||
actor = factories["federation.Actor"]()
|
actor = factories["federation.Actor"]()
|
||||||
channel = factories["audio.Channel"](attributed_to=actor, artist__with_cover=True)
|
artist = factories["music.Artist"](with_cover=True, attributed_to=actor)
|
||||||
track = factories["music.Track"](album__artist=channel.artist)
|
channel = factories["audio.Channel"](attributed_to=actor, artist=artist)
|
||||||
artist = track.artist
|
artist_credit = factories["music.ArtistCredit"](artist=channel.artist)
|
||||||
|
track = factories["music.Track"](
|
||||||
|
album__artist_credit=artist_credit, artist_credit=artist_credit
|
||||||
|
)
|
||||||
|
artist = track.artist_credit.all()[0].artist
|
||||||
artist = artist.__class__.objects.with_albums().get(pk=artist.pk)
|
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))
|
setattr(artist, "_tracks_count", 42)
|
||||||
expected = {
|
expected = {
|
||||||
"id": artist.id,
|
"id": artist.id,
|
||||||
"fid": artist.fid,
|
"fid": artist.fid,
|
||||||
|
@ -110,7 +117,9 @@ def test_artist_with_albums_serializer_channel(factories, to_api_date):
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
serializer = serializers.ArtistWithAlbumsSerializer(artist)
|
serializer = serializers.ArtistWithAlbumsSerializer(artist)
|
||||||
assert serializer.data == expected
|
if not serializer.data == expected:
|
||||||
|
"lol"
|
||||||
|
assert serializer.data == expected
|
||||||
|
|
||||||
|
|
||||||
def test_upload_serializer(factories, to_api_date):
|
def test_upload_serializer(factories, to_api_date):
|
||||||
|
@ -177,7 +186,9 @@ def test_album_serializer(factories, to_api_date):
|
||||||
"fid": album.fid,
|
"fid": album.fid,
|
||||||
"mbid": str(album.mbid),
|
"mbid": str(album.mbid),
|
||||||
"title": album.title,
|
"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),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"is_playable": False,
|
"is_playable": False,
|
||||||
"duration": 0,
|
"duration": 0,
|
||||||
|
@ -207,7 +218,9 @@ def test_track_album_serializer(factories, to_api_date):
|
||||||
"fid": album.fid,
|
"fid": album.fid,
|
||||||
"mbid": str(album.mbid),
|
"mbid": str(album.mbid),
|
||||||
"title": album.title,
|
"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),
|
"creation_date": to_api_date(album.creation_date),
|
||||||
"is_playable": False,
|
"is_playable": False,
|
||||||
"cover": common_serializers.AttachmentSerializer(album.attachment_cover).data,
|
"cover": common_serializers.AttachmentSerializer(album.attachment_cover).data,
|
||||||
|
@ -239,7 +252,9 @@ def test_track_serializer(factories, to_api_date):
|
||||||
expected = {
|
expected = {
|
||||||
"id": track.id,
|
"id": track.id,
|
||||||
"fid": track.fid,
|
"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,
|
"album": serializers.TrackAlbumSerializer(track.album).data,
|
||||||
"mbid": str(track.mbid),
|
"mbid": str(track.mbid),
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
|
|
|
@ -41,7 +41,10 @@ def test_library_track(spa_html, no_api_auth, client, factories, settings):
|
||||||
"property": "music:musician",
|
"property": "music:musician",
|
||||||
"content": utils.join_url(
|
"content": utils.join_url(
|
||||||
settings.FUNKWHALE_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",
|
"property": "music:musician",
|
||||||
"content": utils.join_url(
|
"content": utils.join_url(
|
||||||
settings.FUNKWHALE_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):
|
def test_library_artist(spa_html, no_api_auth, client, factories, settings):
|
||||||
album = factories["music.Album"](with_cover=True)
|
album = factories["music.Album"](with_cover=True)
|
||||||
factories["music.Upload"](playable=True, track__album=album)
|
factories["music.Upload"](playable=True, track__album=album)
|
||||||
artist = album.artist
|
artist = album.artist_credit.all()[0].artist
|
||||||
url = f"/library/artists/{artist.pk}"
|
url = f"/library/artists/{artist.pk}"
|
||||||
|
|
||||||
response = client.get(url)
|
response = client.get(url)
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -7,7 +7,7 @@ import uuid
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
import pytest
|
import pytest
|
||||||
from django.db.models import Prefetch
|
from django.db.models import Count, Prefetch
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
@ -26,12 +26,12 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
track = factories["music.Upload"](
|
track = factories["music.Upload"](
|
||||||
library__privacy_level="everyone",
|
library__privacy_level="everyone",
|
||||||
import_status="finished",
|
import_status="finished",
|
||||||
track__album__artist__set_tags=tags,
|
track__album__artist_credit__artist__set_tags=tags,
|
||||||
).track
|
).track
|
||||||
artist = track.artist
|
artist = track.artist_credit.all()[0].artist
|
||||||
request = api_request.get("/")
|
request = api_request.get("/")
|
||||||
qs = artist.__class__.objects.with_albums().prefetch_related(
|
qs = artist.__class__.objects.with_albums().annotate(
|
||||||
Prefetch("tracks", to_attr="_prefetched_tracks")
|
_tracks_count=Count("artist_credit__tracks")
|
||||||
)
|
)
|
||||||
serializer = serializers.ArtistWithAlbumsSerializer(
|
serializer = serializers.ArtistWithAlbumsSerializer(
|
||||||
qs, many=True, context={"request": request}
|
qs, many=True, context={"request": request}
|
||||||
|
@ -39,12 +39,11 @@ def test_artist_list_serializer(api_request, factories, logged_in_api_client):
|
||||||
expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
|
expected = {"count": 1, "next": None, "previous": None, "results": serializer.data}
|
||||||
for artist in serializer.data:
|
for artist in serializer.data:
|
||||||
artist["tags"] = tags
|
artist["tags"] = tags
|
||||||
for album in artist["albums"]:
|
|
||||||
album["is_playable"] = True
|
|
||||||
|
|
||||||
url = reverse("api:v1:artists-list")
|
url = reverse("api:v1:artists-list")
|
||||||
response = logged_in_api_client.get(url)
|
response = logged_in_api_client.get(url)
|
||||||
|
|
||||||
|
assert serializer.data[0]["tracks_count"] == 1
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
|
||||||
|
@ -118,7 +117,9 @@ def test_artist_view_filter_playable(param, expected, factories, api_request):
|
||||||
"empty": factories["music.Artist"](),
|
"empty": factories["music.Artist"](),
|
||||||
"full": factories["music.Upload"](
|
"full": factories["music.Upload"](
|
||||||
library__privacy_level="everyone", import_status="finished"
|
library__privacy_level="everyone", import_status="finished"
|
||||||
).track.artist,
|
)
|
||||||
|
.track.artist_credit.all()[0]
|
||||||
|
.artist,
|
||||||
}
|
}
|
||||||
|
|
||||||
request = api_request.get("/", {"playable": param})
|
request = api_request.get("/", {"playable": param})
|
||||||
|
@ -817,7 +818,7 @@ def test_user_can_create_upload_in_channel(
|
||||||
channel = factories["audio.Channel"](attributed_to=actor)
|
channel = factories["audio.Channel"](attributed_to=actor)
|
||||||
url = reverse("api:v1:uploads-list")
|
url = reverse("api:v1:uploads-list")
|
||||||
m = mocker.patch("funkwhale_api.common.utils.on_commit")
|
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(
|
response = logged_in_api_client.post(
|
||||||
url,
|
url,
|
||||||
{
|
{
|
||||||
|
@ -968,12 +969,14 @@ def test_can_get_libraries_for_music_entities(
|
||||||
library = upload.library
|
library = upload.library
|
||||||
setattr(library, "_uploads_count", 1)
|
setattr(library, "_uploads_count", 1)
|
||||||
data = {
|
data = {
|
||||||
"artist": upload.track.artist,
|
"artist": upload.track.artist_credit.all()[0].artist,
|
||||||
"album": upload.track.album,
|
"album": upload.track.album,
|
||||||
"track": upload.track,
|
"track": upload.track,
|
||||||
}
|
}
|
||||||
# libraries in channel should be missing excluded
|
# 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"](
|
factories["music.Upload"](
|
||||||
library=channel.library, playable=True, track=upload.track
|
library=channel.library, playable=True, track=upload.track
|
||||||
)
|
)
|
||||||
|
@ -1035,7 +1038,7 @@ def test_oembed_track(factories, no_api_auth, api_client, settings):
|
||||||
"provider_url": settings.FUNKWHALE_URL,
|
"provider_url": settings.FUNKWHALE_URL,
|
||||||
"height": 150,
|
"height": 150,
|
||||||
"width": 600,
|
"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,
|
"description": track.full_name,
|
||||||
"thumbnail_url": federation_utils.full_url(
|
"thumbnail_url": federation_utils.full_url(
|
||||||
track.album.attachment_cover.file.crop["200x200"].url
|
track.album.attachment_cover.file.crop["200x200"].url
|
||||||
|
@ -1045,9 +1048,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(
|
"html": '<iframe width="600" height="150" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
|
||||||
iframe_src
|
iframe_src
|
||||||
),
|
),
|
||||||
"author_name": track.artist.name,
|
"author_name": track.artist_credit.all()[0].artist.name,
|
||||||
"author_url": federation_utils.full_url(
|
"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 +1076,8 @@ def test_oembed_album(factories, no_api_auth, api_client, settings):
|
||||||
"provider_url": settings.FUNKWHALE_URL,
|
"provider_url": settings.FUNKWHALE_URL,
|
||||||
"height": 400,
|
"height": 400,
|
||||||
"width": 600,
|
"width": 600,
|
||||||
"title": 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.name}",
|
"description": f"{album.title} by {album.artist_credit.all()[0].artist.name}",
|
||||||
"thumbnail_url": federation_utils.full_url(
|
"thumbnail_url": federation_utils.full_url(
|
||||||
album.attachment_cover.file.crop["200x200"].url
|
album.attachment_cover.file.crop["200x200"].url
|
||||||
),
|
),
|
||||||
|
@ -1081,9 +1086,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(
|
"html": '<iframe width="600" height="400" scrolling="no" frameborder="no" src="{}"></iframe>'.format(
|
||||||
iframe_src
|
iframe_src
|
||||||
),
|
),
|
||||||
"author_name": album.artist.name,
|
"author_name": album.artist_credit.all()[0].artist.name,
|
||||||
"author_url": federation_utils.full_url(
|
"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 +1104,7 @@ def test_oembed_artist(factories, no_api_auth, api_client, settings):
|
||||||
settings.FUNKWHALE_EMBED_URL = "http://embed"
|
settings.FUNKWHALE_EMBED_URL = "http://embed"
|
||||||
track = factories["music.Track"](album__with_cover=True)
|
track = factories["music.Track"](album__with_cover=True)
|
||||||
album = track.album
|
album = track.album
|
||||||
artist = track.artist
|
artist = track.artist_credit.all()[0].artist
|
||||||
url = reverse("api:v1:oembed")
|
url = reverse("api:v1:oembed")
|
||||||
artist_url = f"https://test.com/library/artists/{artist.pk}"
|
artist_url = f"https://test.com/library/artists/{artist.pk}"
|
||||||
iframe_src = f"http://embed?type=artist&id={artist.pk}"
|
iframe_src = f"http://embed?type=artist&id={artist.pk}"
|
||||||
|
@ -1281,7 +1288,7 @@ def test_artist_list_exclude_channels(
|
||||||
)
|
)
|
||||||
def test_album_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
def test_album_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||||
channel_artist = factories["audio.Channel"]().artist
|
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")
|
url = reverse("api:v1:albums-list")
|
||||||
response = logged_in_api_client.get(url, params)
|
response = logged_in_api_client.get(url, params)
|
||||||
|
@ -1296,7 +1303,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):
|
def test_track_list_exclude_channels(params, expected, factories, logged_in_api_client):
|
||||||
channel_artist = factories["audio.Channel"]().artist
|
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")
|
url = reverse("api:v1:tracks-list")
|
||||||
response = logged_in_api_client.get(url, params)
|
response = logged_in_api_client.get(url, params)
|
||||||
|
@ -1404,10 +1411,12 @@ def test_channel_owner_can_create_album(factories, logged_in_api_client):
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"](attributed_to=actor, artist__with_cover=True)
|
channel = factories["audio.Channel"](attributed_to=actor, artist__with_cover=True)
|
||||||
attachment = factories["common.Attachment"](actor=actor)
|
attachment = factories["common.Attachment"](actor=actor)
|
||||||
|
ac = factories["music.ArtistCredit"](artist=channel.artist)
|
||||||
|
|
||||||
url = reverse("api:v1:albums-list")
|
url = reverse("api:v1:albums-list")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"artist": channel.artist.pk,
|
"artist_credit": ac.pk,
|
||||||
"cover": attachment.uuid,
|
"cover": attachment.uuid,
|
||||||
"title": "Hello world",
|
"title": "Hello world",
|
||||||
"release_date": "2019-01-02",
|
"release_date": "2019-01-02",
|
||||||
|
@ -1417,10 +1426,9 @@ def test_channel_owner_can_create_album(factories, logged_in_api_client):
|
||||||
|
|
||||||
response = logged_in_api_client.post(url, data, format="json")
|
response = logged_in_api_client.post(url, data, format="json")
|
||||||
|
|
||||||
assert response.status_code == 201
|
assert response.status_code == 204
|
||||||
|
|
||||||
album = channel.artist.albums.get(title=data["title"])
|
|
||||||
|
|
||||||
|
album = channel.artist.artist_credit.albums().get(title=data["title"])
|
||||||
assert (
|
assert (
|
||||||
response.data
|
response.data
|
||||||
== serializers.AlbumSerializer(album, context={"description": True}).data
|
== serializers.AlbumSerializer(album, context={"description": True}).data
|
||||||
|
@ -1437,7 +1445,7 @@ def test_channel_owner_can_delete_album(factories, logged_in_api_client, mocker)
|
||||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"](attributed_to=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})
|
url = reverse("api:v1:albums-detail", kwargs={"pk": album.pk})
|
||||||
|
|
||||||
response = logged_in_api_client.delete(url)
|
response = logged_in_api_client.delete(url)
|
||||||
|
@ -1474,7 +1482,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):
|
def test_other_user_cannot_delete_album(factories, logged_in_api_client):
|
||||||
logged_in_api_client.user.create_actor()
|
logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"]()
|
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})
|
url = reverse("api:v1:albums-detail", kwargs={"pk": album.pk})
|
||||||
|
|
||||||
response = logged_in_api_client.delete(url)
|
response = logged_in_api_client.delete(url)
|
||||||
|
@ -1487,7 +1495,7 @@ def test_channel_owner_can_delete_track(factories, logged_in_api_client, mocker)
|
||||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"](attributed_to=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)
|
upload1 = factories["music.Upload"](track=track)
|
||||||
upload2 = factories["music.Upload"](track=track)
|
upload2 = factories["music.Upload"](track=track)
|
||||||
url = reverse("api:v1:tracks-detail", kwargs={"pk": track.pk})
|
url = reverse("api:v1:tracks-detail", kwargs={"pk": track.pk})
|
||||||
|
@ -1506,7 +1514,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):
|
def test_other_user_cannot_delete_track(factories, logged_in_api_client):
|
||||||
logged_in_api_client.user.create_actor()
|
logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"]()
|
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})
|
url = reverse("api:v1:tracks-detail", kwargs={"pk": track.pk})
|
||||||
|
|
||||||
response = logged_in_api_client.delete(url)
|
response = logged_in_api_client.delete(url)
|
||||||
|
@ -1597,3 +1605,13 @@ def test_fs_import_cancel_already_running(
|
||||||
|
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
assert cache.get("fs-import:status") == "canceled"
|
assert cache.get("fs-import:status") == "canceled"
|
||||||
|
|
||||||
|
|
||||||
|
def test_album_create_artist_credit(factories, logged_in_api_client):
|
||||||
|
artist = factories["music.Artist"]()
|
||||||
|
factories["audio.Channel"](artist=artist)
|
||||||
|
url = reverse("api:v1:albums-list")
|
||||||
|
response = logged_in_api_client.post(
|
||||||
|
url, {"artist": artist.pk, "title": "super album"}, format="json"
|
||||||
|
)
|
||||||
|
assert response.status_code == 204
|
||||||
|
|
|
@ -23,6 +23,8 @@ def test_playlist_filter_artist(factories, queryset_equal_list):
|
||||||
plt = factories["playlists.PlaylistTrack"]()
|
plt = factories["playlists.PlaylistTrack"]()
|
||||||
factories["playlists.PlaylistTrack"]()
|
factories["playlists.PlaylistTrack"]()
|
||||||
qs = models.Playlist.objects.all()
|
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]
|
assert filterset.qs == [plt.playlist]
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -20,9 +22,11 @@ def test_can_list_config_options(logged_in_api_client):
|
||||||
def test_can_validate_config(logged_in_api_client, factories):
|
def test_can_validate_config(logged_in_api_client, factories):
|
||||||
artist1 = factories["music.Artist"]()
|
artist1 = factories["music.Artist"]()
|
||||||
artist2 = factories["music.Artist"]()
|
artist2 = factories["music.Artist"]()
|
||||||
factories["music.Track"].create_batch(3, artist=artist1)
|
factories["music.Track"].create_batch(3, artist_credit__artist=artist1)
|
||||||
factories["music.Track"].create_batch(3, artist=artist2)
|
factories["music.Track"].create_batch(3, artist_credit__artist=artist2)
|
||||||
candidates = artist1.tracks.order_by("pk")
|
candidates = list(
|
||||||
|
chain(*[ac.tracks.order_by("pk") for ac in artist1.artist_credit.all()])
|
||||||
|
)
|
||||||
f = {"filters": [{"type": "artist", "ids": [artist1.pk]}]}
|
f = {"filters": [{"type": "artist", "ids": [artist1.pk]}]}
|
||||||
url = reverse("api:v1:radios:radios-validate")
|
url = reverse("api:v1:radios:radios-validate")
|
||||||
response = logged_in_api_client.post(url, f, format="json")
|
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
|
payload = response.data
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"count": candidates.count(),
|
"count": len(candidates),
|
||||||
"sample": TrackSerializer(candidates, many=True).data,
|
"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):
|
def test_can_get_choices_for_custom_radio(factories):
|
||||||
artist = factories["music.Artist"]()
|
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]
|
tracks = [f.track for f in files]
|
||||||
factories["music.Upload"].create_batch(5)
|
factories["music.Upload"].create_batch(5)
|
||||||
|
|
||||||
|
@ -208,7 +210,9 @@ def test_can_start_artist_radio(factories):
|
||||||
user = factories["users.User"]()
|
user = factories["users.User"]()
|
||||||
artist = factories["music.Artist"]()
|
artist = factories["music.Artist"]()
|
||||||
factories["music.Upload"].create_batch(5)
|
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]
|
good_tracks = [f.track for f in good_files]
|
||||||
|
|
||||||
radio = radios.ArtistRadio()
|
radio = radios.ArtistRadio()
|
||||||
|
@ -224,7 +228,7 @@ def test_can_start_tag_radio(factories):
|
||||||
good_tracks = [
|
good_tracks = [
|
||||||
factories["music.Track"](set_tags=[tag.name]),
|
factories["music.Track"](set_tags=[tag.name]),
|
||||||
factories["music.Track"](album__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"])
|
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
|
factories, queryset_equal_list
|
||||||
):
|
):
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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"]()
|
valid_track = factories["music.Track"]()
|
||||||
radio = radios.RandomRadio()
|
radio = radios.RandomRadio()
|
||||||
radio.start_session(user=cf.user)
|
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
|
factories, queryset_equal_list
|
||||||
):
|
):
|
||||||
cf = factories["moderation.UserFilter"](for_artist=True)
|
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"]()
|
valid_track = factories["music.Track"]()
|
||||||
radio = radios.RandomRadio()
|
radio = radios.RandomRadio()
|
||||||
radio.start_session(user=cf.user)
|
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"]()
|
included_artist = factories["music.Artist"]()
|
||||||
excluded_artist = factories["music.Artist"]()
|
excluded_artist = factories["music.Artist"]()
|
||||||
included_uploads = factories["music.Upload"].create_batch(
|
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"](
|
session = factories["radios.CustomRadioSession"](
|
||||||
custom_radio__config=[
|
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"}
|
map_filters_to_type = {"tags": "names", "artists": "ids", "playlists": "names"}
|
||||||
for key, value in map_filters_to_type.items():
|
for key, value in map_filters_to_type.items():
|
||||||
attr = value[:-1]
|
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}]}
|
config = {"filters": [{"type": key, value: track_filter_key}]}
|
||||||
response = api_client.post(
|
response = api_client.post(
|
||||||
url,
|
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):
|
def test_can_get_choices_for_custom_radio_v2(factories):
|
||||||
artist = factories["music.Artist"]()
|
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]
|
tracks = [f.track for f in files]
|
||||||
factories["music.Upload"].create_batch(5)
|
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",
|
"album__title": "World",
|
||||||
"title": "foo",
|
"title": "foo",
|
||||||
"position": None,
|
"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",
|
"album__title": "escape/my",
|
||||||
"title": "sla/sh",
|
"title": "sla/sh",
|
||||||
"position": 12,
|
"position": 12,
|
||||||
|
@ -76,8 +76,8 @@ def test_get_artists_serializer(factories):
|
||||||
name="", with_cover=False
|
name="", with_cover=False
|
||||||
) # Shouldn't be serialised
|
) # Shouldn't be serialised
|
||||||
|
|
||||||
factories["music.Album"].create_batch(size=3, artist=artist1)
|
factories["music.Album"].create_batch(size=3, artist_credit__artist=artist1)
|
||||||
factories["music.Album"].create_batch(size=2, artist=artist2)
|
factories["music.Album"].create_batch(size=2, artist_credit__artist=artist2)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
"ignoredArticles": "",
|
"ignoredArticles": "",
|
||||||
|
@ -125,7 +125,7 @@ def test_get_artists_serializer(factories):
|
||||||
|
|
||||||
def test_get_artist_serializer(factories):
|
def test_get_artist_serializer(factories):
|
||||||
artist = factories["music.Artist"](with_cover=True)
|
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)
|
tracks = factories["music.Track"].create_batch(size=3, album=album)
|
||||||
|
|
||||||
expected = {
|
expected = {
|
||||||
|
@ -191,7 +191,7 @@ def test_get_track_data_content_type(mimetype, extension, expected, factories):
|
||||||
|
|
||||||
def test_get_album_serializer(factories):
|
def test_get_album_serializer(factories):
|
||||||
artist = factories["music.Artist"]()
|
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)
|
track = factories["music.Track"](album=album, disc_number=42)
|
||||||
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
|
upload = factories["music.Upload"](track=track, bitrate=42000, duration=43, size=44)
|
||||||
tagged_item = factories["tags.TaggedItem"](content_object=album, tag__name="foo")
|
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):
|
def test_starred_tracks2_serializer(factories):
|
||||||
artist = factories["music.Artist"]()
|
artist_credit = factories["music.ArtistCredit"]()
|
||||||
album = factories["music.Album"](artist=artist)
|
album = factories["music.Album"](artist_credit=artist_credit)
|
||||||
track = factories["music.Track"](album=album)
|
track = factories["music.Track"](album=album)
|
||||||
upload = factories["music.Upload"](track=track)
|
upload = factories["music.Upload"](track=track)
|
||||||
favorite = factories["favorites.TrackFavorite"](track=track)
|
favorite = factories["favorites.TrackFavorite"](track=track)
|
||||||
|
@ -347,7 +347,7 @@ def test_channel_episode_serializer(factories):
|
||||||
description = factories["common.Content"]()
|
description = factories["common.Content"]()
|
||||||
channel = factories["audio.Channel"]()
|
channel = factories["audio.Channel"]()
|
||||||
track = factories["music.Track"](
|
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"](
|
upload = factories["music.Upload"](
|
||||||
playable=True, track=track, bitrate=128000, duration=42
|
playable=True, track=track, bitrate=128000, duration=42
|
||||||
|
|
|
@ -152,7 +152,9 @@ def test_get_artist(
|
||||||
url = reverse("api:subsonic:subsonic-get_artist")
|
url = reverse("api:subsonic:subsonic-get_artist")
|
||||||
assert url.endswith("getArtist") is True
|
assert url.endswith("getArtist") is True
|
||||||
artist = factories["music.Artist"](playable=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")
|
playable_by = mocker.spy(music_models.ArtistQuerySet, "playable_by")
|
||||||
|
|
||||||
expected = {"artist": serializers.GetArtistSerializer(artist).data}
|
expected = {"artist": serializers.GetArtistSerializer(artist).data}
|
||||||
|
@ -202,9 +204,9 @@ def test_get_album(
|
||||||
):
|
):
|
||||||
url = reverse("api:subsonic:subsonic-get_album")
|
url = reverse("api:subsonic:subsonic-get_album")
|
||||||
assert url.endswith("getAlbum") is True
|
assert url.endswith("getAlbum") is True
|
||||||
artist = factories["music.Artist"]()
|
artist_credit = factories["music.ArtistCredit"]()
|
||||||
album = (
|
album = (
|
||||||
factories["music.Album"](artist=artist)
|
factories["music.Album"](artist_credit=artist_credit)
|
||||||
.__class__.objects.with_duration()
|
.__class__.objects.with_duration()
|
||||||
.first()
|
.first()
|
||||||
)
|
)
|
||||||
|
@ -217,7 +219,10 @@ def test_get_album(
|
||||||
assert response.data == expected
|
assert response.data == expected
|
||||||
|
|
||||||
playable_by.assert_called_once_with(
|
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")
|
url = reverse("api:subsonic:subsonic-get_song")
|
||||||
assert url.endswith("getSong") is True
|
assert url.endswith("getSong") is True
|
||||||
artist = factories["music.Artist"]()
|
artist_credit = factories["music.ArtistCredit"]()
|
||||||
album = factories["music.Album"](artist=artist)
|
album = factories["music.Album"](artist_credit=artist_credit)
|
||||||
track = factories["music.Track"](album=album, playable=True)
|
track = factories["music.Track"](album=album, playable=True)
|
||||||
upload = factories["music.Upload"](track=track)
|
upload = factories["music.Upload"](track=track)
|
||||||
playable_by = mocker.spy(music_models.TrackQuerySet, "playable_by")
|
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")
|
url = reverse("api:subsonic:subsonic-get_album_list2")
|
||||||
assert url.endswith("getAlbumList2") is True
|
assert url.endswith("getAlbumList2") is True
|
||||||
album1 = factories["music.Album"](
|
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]
|
).__class__.objects.with_duration()[0]
|
||||||
album2 = factories["music.Album"](
|
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]
|
).__class__.objects.with_duration()[1]
|
||||||
factories["music.Album"](playable=True, set_tags=["Pop"])
|
factories["music.Album"](playable=True, set_tags=["Pop"])
|
||||||
response = logged_in_api_client.get(
|
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("f", ["json"])
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"tags_field",
|
"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):
|
def test_get_songs_by_genre(f, tags_field, db, logged_in_api_client, factories):
|
||||||
url = reverse("api:subsonic:subsonic-get_songs_by_genre")
|
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"](
|
upload1 = factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
bitrate=128000,
|
bitrate=128000,
|
||||||
duration=42,
|
duration=42,
|
||||||
)
|
)
|
||||||
upload2 = factories["music.Upload"](
|
upload2 = factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
duration=43,
|
duration=43,
|
||||||
)
|
)
|
||||||
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
|
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["audio.Channel"](external=True)
|
||||||
factories["federation.Follow"]()
|
factories["federation.Follow"]()
|
||||||
url = reverse("api:subsonic:subsonic-get_podcasts")
|
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"](
|
upload1 = factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel1.artist,
|
track__artist_credit__artist=channel1.artist,
|
||||||
library=channel1.library,
|
library=channel1.library,
|
||||||
bitrate=128000,
|
bitrate=128000,
|
||||||
duration=42,
|
duration=42,
|
||||||
)
|
)
|
||||||
factories["music.Upload"](
|
factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel2.artist,
|
track__artist_credit__artist=channel2.artist,
|
||||||
library=channel2.library,
|
library=channel2.library,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
duration=43,
|
duration=43,
|
||||||
|
@ -983,14 +997,14 @@ def test_get_newest_podcasts(logged_in_api_client, factories, mocker):
|
||||||
)
|
)
|
||||||
upload1 = factories["music.Upload"](
|
upload1 = factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
bitrate=128000,
|
bitrate=128000,
|
||||||
duration=42,
|
duration=42,
|
||||||
)
|
)
|
||||||
upload2 = factories["music.Upload"](
|
upload2 = factories["music.Upload"](
|
||||||
playable=True,
|
playable=True,
|
||||||
track__artist=channel.artist,
|
track__artist_credit__artist=channel.artist,
|
||||||
library=channel.library,
|
library=channel.library,
|
||||||
bitrate=256000,
|
bitrate=256000,
|
||||||
duration=43,
|
duration=43,
|
||||||
|
|
|
@ -6,9 +6,11 @@ def test_get_tags_from_foreign_key(factories):
|
||||||
rock_tag = factories["tags.Tag"](name="Rock")
|
rock_tag = factories["tags.Tag"](name="Rock")
|
||||||
rap_tag = factories["tags.Tag"](name="Rap")
|
rap_tag = factories["tags.Tag"](name="Rap")
|
||||||
artist = factories["music.Artist"]()
|
artist = factories["music.Artist"]()
|
||||||
factories["music.Track"].create_batch(3, artist=artist, set_tags=["rock", "rap"])
|
|
||||||
factories["music.Track"].create_batch(
|
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(
|
result = tasks.get_tags_from_foreign_key(
|
||||||
|
|
|
@ -11,13 +11,13 @@ def test_resolve_recordings_to_fw_track(mocker, factories):
|
||||||
factories["music.Track"](
|
factories["music.Track"](
|
||||||
pk=1,
|
pk=1,
|
||||||
title="I Want It That Way",
|
title="I Want It That Way",
|
||||||
artist=artist,
|
artist_credit__artist=artist,
|
||||||
mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa",
|
mbid="87dfa566-21c3-45ed-bc42-1d345b8563fa",
|
||||||
)
|
)
|
||||||
factories["music.Track"](
|
factories["music.Track"](
|
||||||
pk=2,
|
pk=2,
|
||||||
title="I Want It That Way",
|
title="I Want It That Way",
|
||||||
artist=artist,
|
artist_credit__artist=artist,
|
||||||
)
|
)
|
||||||
|
|
||||||
client = typesense.Client(
|
client = typesense.Client(
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Support multiples artists for tracks and albums (#1568)
|
|
@ -241,12 +241,12 @@ Given the above example, Funkwhale would create the following `ArtistCredit` obj
|
||||||
|
|
||||||
The Funkwhale API needs to return artist credit information in a way that is easily consumed by a client.
|
The Funkwhale API needs to return artist credit information in a way that is easily consumed by a client.
|
||||||
|
|
||||||
Endpoints should include a `credited_artist` filter that allows a client to return results for which artists are credited. This filter should take a list of IDs.
|
Endpoints should include a `artist` filter that allows a client to return results for which artists are credited. This filter should take a list of IDs.
|
||||||
|
|
||||||
To return any albums where the artist is listed in the `artist_credit` field, you can filter by the `artist_id` field using the `credited_artist` filter:
|
To return any albums where the artist is listed in the `artist_credit` field, you can filter by the `artist_id` field using the `artist` filter:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
https://open.audio/api/v2/albums?credited_artist=6451,6452
|
https://open.audio/api/v2/albums?artist=6451,6452
|
||||||
```
|
```
|
||||||
|
|
||||||
The `credit` field of the `artist_credit` object must also be searchable using a standard query:
|
The `credit` field of the `artist_credit` object must also be searchable using a standard query:
|
||||||
|
|
|
@ -32,7 +32,7 @@ const getTrackInformationText = (track: QueueTrack | undefined) => {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
return `♫ ${track.title} – ${track.artistName} ♫`
|
return `♫ ${track.title} – ${track.artistCredit} ♫`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update title
|
// Update title
|
||||||
|
|
|
@ -130,14 +130,13 @@ const reorderTracks = async (from: number, to: number) => {
|
||||||
scrollToCurrent()
|
scrollToCurrent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hideArtist = () => {
|
const hideArtist = () => {
|
||||||
if (currentTrack.value.artistId !== -1) {
|
if (currentTrack.value.artistId !== -1 && currentTrack.value.artistCredit) {
|
||||||
return store.dispatch('moderation/hide', {
|
return store.dispatch('moderation/hide', {
|
||||||
type: 'artist',
|
type: 'artist',
|
||||||
target: {
|
target: {
|
||||||
id: currentTrack.value.artistId,
|
id: currentTrack.value.artistCredit[0].artist.id,
|
||||||
name: currentTrack.value.artistName
|
name: currentTrack.value.artistCredit[0].artist.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -264,7 +263,13 @@ if (!isWebGLSupported) {
|
||||||
>
|
>
|
||||||
<h1>{{ currentTrack.title }}</h1>
|
<h1>{{ currentTrack.title }}</h1>
|
||||||
<h2>
|
<h2>
|
||||||
{{ currentTrack.artistName ?? $t('components.Queue.meta.unknownArtist') }}
|
<div
|
||||||
|
v-for="ac in currentTrack.artistCredit"
|
||||||
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
|
{{ ac.credit ?? $t('components.Queue.meta.unknownArtist') }}
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</div>
|
||||||
<span class="symbol hyphen middle" />
|
<span class="symbol hyphen middle" />
|
||||||
{{ currentTrack.albumTitle ?? $t('components.Queue.meta.unknownAlbum') }}
|
{{ currentTrack.albumTitle ?? $t('components.Queue.meta.unknownAlbum') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -281,12 +286,21 @@ if (!isWebGLSupported) {
|
||||||
{{ currentTrack.title }}
|
{{ currentTrack.title }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<div class="sub header ellipsis">
|
<div class="sub header ellipsis">
|
||||||
<router-link
|
<span>
|
||||||
class="discrete link artist"
|
<template
|
||||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
|
v-for="ac in currentTrack.artistCredit"
|
||||||
>
|
:key="ac.artist.id"
|
||||||
{{ currentTrack.artistName ?? $t('components.Queue.meta.unknownArtist') }}
|
>
|
||||||
</router-link>
|
<router-link
|
||||||
|
class="discrete link"
|
||||||
|
:to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
|
||||||
|
@click.stop.prevent=""
|
||||||
|
>
|
||||||
|
{{ ac.credit ?? $t('components.Queue.meta.unknownArtist') }}
|
||||||
|
</router-link>
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
<template v-if="currentTrack.albumId !== -1">
|
<template v-if="currentTrack.albumId !== -1">
|
||||||
<span class="middle slash symbol" />
|
<span class="middle slash symbol" />
|
||||||
<router-link
|
<router-link
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import type { QueueItemSource } from '~/types'
|
import type { QueueItemSource } from '~/types'
|
||||||
|
|
||||||
import time from '~/utils/time'
|
import time from '~/utils/time'
|
||||||
|
import { generateTrackCreditStringFromQueue } from '~/utils/utils'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'play', index: number): void
|
(e: 'play', index: number): void
|
||||||
|
@ -43,7 +44,7 @@ defineProps<Props>()
|
||||||
>
|
>
|
||||||
<strong>{{ source.title }}</strong><br>
|
<strong>{{ source.title }}</strong><br>
|
||||||
<span>
|
<span>
|
||||||
{{ source.artistName }}
|
{{ generateTrackCreditStringFromQueue(source) }}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { ArtistCredit } from '~/types'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
artistCredit: ArtistCredit[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const getRoute = (ac: ArtistCredit) => {
|
||||||
|
return {
|
||||||
|
name: ac.artist.channel ? 'channels.detail' : 'library.artists.detail',
|
||||||
|
params: {
|
||||||
|
id: ac.artist.id.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="artist-label ui image label">
|
||||||
|
<template
|
||||||
|
v-for="ac in props.artistCredit"
|
||||||
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
:to="getRoute(ac)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="ac.index === 0 && ac.artist.cover && ac.artist.cover.urls.original"
|
||||||
|
v-lazy="$store.getters['instance/absoluteUrl'](ac.artist.cover.urls.medium_square_crop)"
|
||||||
|
alt=""
|
||||||
|
:class="[{circular: ac.artist.content_category != 'podcast'}]"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-else-if="ac.index === 0"
|
||||||
|
:class="[ac.artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet users icon']"
|
||||||
|
/>
|
||||||
|
{{ ac.credit }}
|
||||||
|
</router-link>
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -43,7 +43,7 @@ const duration = computed(() => props.entry.uploads.find(upload => upload.durati
|
||||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="entry.artist?.content_category === 'podcast' && defaultCover != undefined"
|
v-else-if="entry.artist_credit?.[0].artist.content_category === 'podcast' && defaultCover != undefined"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
|
v-lazy="$store.getters['instance/absoluteUrl'](defaultCover.urls.medium_square_crop)"
|
||||||
class="channel-image image"
|
class="channel-image image"
|
||||||
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
@click="$router.push({name: 'library.tracks.detail', params: {id: entry.id}})"
|
||||||
|
|
|
@ -199,7 +199,7 @@ const openMenu = () => {
|
||||||
@click.stop.prevent="$router.push(`/library/tracks/${track?.id}/`)"
|
@click.stop.prevent="$router.push(`/library/tracks/${track?.id}/`)"
|
||||||
>
|
>
|
||||||
<i class="info icon" />
|
<i class="info icon" />
|
||||||
<span v-if="track.artist?.content_category === 'podcast'">
|
<span v-if="track.artist_credit?.some(ac => ac.artist.content_category === 'podcast')">
|
||||||
{{ $t('components.audio.PlayButton.button.episodeDetails') }}
|
{{ $t('components.audio.PlayButton.button.episodeDetails') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
|
|
@ -109,12 +109,12 @@ const loopingTitle = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const hideArtist = () => {
|
const hideArtist = () => {
|
||||||
if (currentTrack.value.artistId !== -1) {
|
if (currentTrack.value.artistId !== -1 && currentTrack.value.artistCredit) {
|
||||||
return store.dispatch('moderation/hide', {
|
return store.dispatch('moderation/hide', {
|
||||||
type: 'artist',
|
type: 'artist',
|
||||||
target: {
|
target: {
|
||||||
id: currentTrack.value.artistId,
|
id: currentTrack.value.artistCredit[0].artist.id,
|
||||||
name: currentTrack.value.artistName
|
name: currentTrack.value.artistCredit[0].artist.name
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -178,13 +178,21 @@ const hideArtist = () => {
|
||||||
</router-link>
|
</router-link>
|
||||||
</strong>
|
</strong>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
<router-link
|
<span>
|
||||||
class="discrete link"
|
<template
|
||||||
:to="{name: 'library.artists.detail', params: {id: currentTrack.artistId }}"
|
v-for="ac in currentTrack.artistCredit"
|
||||||
@click.stop.prevent=""
|
:key="ac.artist.id"
|
||||||
>
|
>
|
||||||
{{ currentTrack.artistName ?? $t('components.audio.Player.meta.unknownArtist') }}
|
<router-link
|
||||||
</router-link>
|
class="discrete link"
|
||||||
|
:to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
|
||||||
|
@click.stop.prevent=""
|
||||||
|
>
|
||||||
|
{{ ac.credit ?? $t('components.audio.Player.meta.unknownArtist') }}
|
||||||
|
</router-link>
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
<template v-if="currentTrack.albumId !== -1">
|
<template v-if="currentTrack.albumId !== -1">
|
||||||
<span class="middle slash symbol" />
|
<span class="middle slash symbol" />
|
||||||
<router-link
|
<router-link
|
||||||
|
@ -211,7 +219,13 @@ const hideArtist = () => {
|
||||||
{{ currentTrack.title }}
|
{{ currentTrack.title }}
|
||||||
</strong>
|
</strong>
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
{{ currentTrack.artistName ?? $t('components.audio.Player.meta.unknownArtist') }}
|
<div
|
||||||
|
v-for="ac in currentTrack.artistCredit"
|
||||||
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
|
{{ ac.credit ?? $t('components.audio.Player.meta.unknownArtist') }}
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</div>
|
||||||
<template v-if="currentTrack.albumId !== -1">
|
<template v-if="currentTrack.albumId !== -1">
|
||||||
<span class="middle slash symbol" />
|
<span class="middle slash symbol" />
|
||||||
{{ currentTrack.albumTitle ?? $t('components.audio.Player.meta.unknownAlbum') }}
|
{{ currentTrack.albumTitle ?? $t('components.audio.Player.meta.unknownAlbum') }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Artist, Album } from '~/types'
|
import type { Album, ArtistCredit } from '~/types'
|
||||||
|
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { ref, computed, reactive, watch, onMounted } from 'vue'
|
import { ref, computed, reactive, watch, onMounted } from 'vue'
|
||||||
|
@ -27,7 +27,7 @@ const query = ref('')
|
||||||
const queryDebounced = refDebounced(query, 500)
|
const queryDebounced = refDebounced(query, 500)
|
||||||
|
|
||||||
const results = reactive({
|
const results = reactive({
|
||||||
artists: [] as Artist[],
|
artists: [] as ArtistCredit[],
|
||||||
albums: [] as Album[]
|
albums: [] as Album[]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -94,9 +94,9 @@ const labels = computed(() => ({
|
||||||
<div v-if="results.artists.length > 0">
|
<div v-if="results.artists.length > 0">
|
||||||
<div class="ui cards">
|
<div class="ui cards">
|
||||||
<artist-card
|
<artist-card
|
||||||
v-for="artist in results.artists"
|
v-for="ac in results.artists"
|
||||||
:key="artist.id"
|
:key="ac.artist.id"
|
||||||
:artist="artist"
|
:artist="ac.artist"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { ref, computed, onMounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useStore } from '~/store'
|
import { useStore } from '~/store'
|
||||||
|
import { generateTrackCreditString } from '~/utils/utils'
|
||||||
|
|
||||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
|
|
||||||
|
@ -97,7 +98,7 @@ const categories = computed(() => [
|
||||||
name: labels.value.album,
|
name: labels.value.album,
|
||||||
getId: (obj: Album) => obj.id,
|
getId: (obj: Album) => obj.id,
|
||||||
getTitle: (obj: Album) => obj.title,
|
getTitle: (obj: Album) => obj.title,
|
||||||
getDescription: (obj: Album) => obj.artist.name
|
getDescription: (obj: Album) => generateTrackCreditString(obj)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'tracks',
|
code: 'tracks',
|
||||||
|
@ -105,7 +106,10 @@ const categories = computed(() => [
|
||||||
name: labels.value.track,
|
name: labels.value.track,
|
||||||
getId: (obj: Track) => obj.id,
|
getId: (obj: Track) => obj.id,
|
||||||
getTitle: (obj: Track) => obj.title,
|
getTitle: (obj: Track) => obj.title,
|
||||||
getDescription: (obj: Track) => obj.album?.artist.name ?? obj.artist?.name ?? ''
|
getDescription: (track: Track) => {
|
||||||
|
const album = track.album ?? null
|
||||||
|
return generateTrackCreditString(album) ?? generateTrackCreditString(track) ?? ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
code: 'tags',
|
code: 'tags',
|
||||||
|
|
|
@ -48,12 +48,18 @@ const imageUrl = computed(() => props.album.cover?.urls.original
|
||||||
</strong>
|
</strong>
|
||||||
<div class="description">
|
<div class="description">
|
||||||
<span>
|
<span>
|
||||||
<router-link
|
<template
|
||||||
class="discrete link"
|
v-for="ac in album.artist_credit"
|
||||||
:to="{name: 'library.artists.detail', params: {id: album.artist.id}}"
|
:key="ac.artist.id"
|
||||||
>
|
>
|
||||||
{{ album.artist.name }}
|
<router-link
|
||||||
</router-link>
|
class="discrete link"
|
||||||
|
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}"
|
||||||
|
>
|
||||||
|
{{ ac.credit }}
|
||||||
|
</router-link>
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -12,6 +12,7 @@ import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackModal from '~/components/audio/track/Modal.vue'
|
import TrackModal from '~/components/audio/track/Modal.vue'
|
||||||
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
interface Props extends PlayOptionsProps {
|
interface Props extends PlayOptionsProps {
|
||||||
track: Track
|
track: Track
|
||||||
|
@ -81,8 +82,8 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="track.artist?.cover"
|
v-else-if="!!track.artist_credit.length && track.artist_credit[0].artist.cover"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](track.artist.cover.urls.medium_square_crop) "
|
v-lazy="getArtistCoverUrl(track.artist_credit)"
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
|
@ -109,7 +110,7 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
v-if="track.artist?.content_category === 'podcast'"
|
v-if="track.artist_credit?.[0].artist.content_category === 'podcast'"
|
||||||
class="track-meta mobile"
|
class="track-meta mobile"
|
||||||
>
|
>
|
||||||
<human-date
|
<human-date
|
||||||
|
@ -126,7 +127,7 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
|
||||||
v-else
|
v-else
|
||||||
class="track-meta mobile"
|
class="track-meta mobile"
|
||||||
>
|
>
|
||||||
{{ track.artist?.name }}
|
{{ generateTrackCreditString(track) }}
|
||||||
<span class="middledot symbol" />
|
<span class="middledot symbol" />
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
|
@ -135,7 +136,7 @@ const actionsButtonLabel = computed(() => t('components.audio.podcast.MobileRow.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated && track.artist?.content_category !== 'podcast'"
|
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
|
||||||
:class="[
|
:class="[
|
||||||
'meta',
|
'meta',
|
||||||
'right',
|
'right',
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { computed, ref } from 'vue'
|
||||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
import useReport from '~/composables/moderation/useReport'
|
import useReport from '~/composables/moderation/useReport'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'update:show', value: boolean): void
|
(e: 'update:show', value: boolean): void
|
||||||
|
@ -64,17 +65,17 @@ const favoriteButton = computed(() => isFavorite.value
|
||||||
: t('components.audio.podcast.Modal.button.addToFavorites')
|
: t('components.audio.podcast.Modal.button.addToFavorites')
|
||||||
)
|
)
|
||||||
|
|
||||||
const trackDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const trackDetailsButton = computed(() => props.track.artist_credit?.[0].artist.content_category === 'podcast'
|
||||||
? t('components.audio.podcast.Modal.button.episodeDetails')
|
? t('components.audio.podcast.Modal.button.episodeDetails')
|
||||||
: t('components.audio.podcast.Modal.button.trackDetails')
|
: t('components.audio.podcast.Modal.button.trackDetails')
|
||||||
)
|
)
|
||||||
|
|
||||||
const albumDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const albumDetailsButton = computed(() => props.track.artist_credit?.[0].artist?.content_category === 'podcast'
|
||||||
? t('components.audio.podcast.Modal.button.seriesDetails')
|
? t('components.audio.podcast.Modal.button.seriesDetails')
|
||||||
: t('components.audio.podcast.Modal.button.albumDetails')
|
: t('components.audio.podcast.Modal.button.albumDetails')
|
||||||
)
|
)
|
||||||
|
|
||||||
const artistDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const artistDetailsButton = computed(() => props.track.artist_credit?.[0].artist?.content_category === 'podcast'
|
||||||
? t('components.audio.podcast.Modal.button.channelDetails')
|
? t('components.audio.podcast.Modal.button.channelDetails')
|
||||||
: t('components.audio.podcast.Modal.button.artistDetails')
|
: t('components.audio.podcast.Modal.button.artistDetails')
|
||||||
)
|
)
|
||||||
|
@ -118,11 +119,9 @@ const labels = computed(() => ({
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="track.artist?.cover"
|
v-else-if="!!track.artist_credit?.length && track.artist_credit[0].artist.cover"
|
||||||
v-lazy="
|
v-lazy="
|
||||||
$store.getters['instance/absoluteUrl'](
|
getArtistCoverUrl(track.artist_credit)
|
||||||
track.artist.cover.urls.medium_square_crop
|
|
||||||
)
|
|
||||||
"
|
"
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
|
@ -138,14 +137,14 @@ const labels = computed(() => ({
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<h4 class="track-modal-subtitle">
|
<h4 class="track-modal-subtitle">
|
||||||
{{ track.artist?.name }}
|
{{ generateTrackCreditString(track) }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="ui one column unstackable grid">
|
<div class="ui one column unstackable grid">
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated && track.artist?.content_category !== 'podcast'"
|
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist?.content_category !== 'podcast'"
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -254,20 +253,16 @@ const labels = computed(() => ({
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-for="ac in track.artist_credit"
|
||||||
|
:key="ac.artist.id"
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="artistDetailsButton"
|
:aria-label="artistDetailsButton"
|
||||||
@click.prevent.exact="
|
@click.prevent.exact="$router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
|
||||||
$router.push({
|
|
||||||
name: 'library.artists.detail',
|
|
||||||
params: { id: track.artist?.id },
|
|
||||||
})
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<i class="user icon track-modal list-icon" />
|
<i class="user icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{
|
<span class="track-modal list-item">{{ ac.artist.name }}</span>
|
||||||
artistDetailsButton
|
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
||||||
}}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -290,7 +285,7 @@ const labels = computed(() => ({
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider" />
|
<div class="ui divider" />
|
||||||
<div
|
<div
|
||||||
v-for="obj in getReportableObjects({ track, album: track.album, artist: track.artist })"
|
v-for="obj in getReportableObjects({ track, album: track.album, artistCredit: track.artist_credit })"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
class="row"
|
class="row"
|
||||||
@click.stop.prevent="report(obj)"
|
@click.stop.prevent="report(obj)"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Track, Artist, Album, Playlist, Library, Channel, Actor, Cover } from '~/types'
|
import type { Track, Album, Playlist, Library, Channel, Actor, Cover, ArtistCredit } from '~/types'
|
||||||
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
@ -26,7 +26,7 @@ interface Props extends PlayOptionsProps {
|
||||||
|
|
||||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||||
isPlayable?: boolean
|
isPlayable?: boolean
|
||||||
artist?: Artist | null
|
artistCredit?: ArtistCredit[] | null
|
||||||
album?: Album | null
|
album?: Album | null
|
||||||
playlist?: Playlist | null
|
playlist?: Playlist | null
|
||||||
library?: Library | null
|
library?: Library | null
|
||||||
|
@ -40,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
defaultCover: null,
|
defaultCover: null,
|
||||||
|
|
||||||
tracks: () => [],
|
tracks: () => [],
|
||||||
artist: null,
|
artistCredit: null,
|
||||||
album: null,
|
album: null,
|
||||||
playlist: null,
|
playlist: null,
|
||||||
library: null,
|
library: null,
|
||||||
|
|
|
@ -12,6 +12,7 @@ import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackModal from '~/components/audio/track/Modal.vue'
|
import TrackModal from '~/components/audio/track/Modal.vue'
|
||||||
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
interface Props extends PlayOptionsProps {
|
interface Props extends PlayOptionsProps {
|
||||||
track: Track
|
track: Track
|
||||||
|
@ -81,8 +82,8 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="track.artist?.cover"
|
v-else-if="track.artist_credit.length && track.artist_credit[0].artist.cover"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](track.artist.cover.urls.medium_square_crop)"
|
v-lazy="getArtistCoverUrl(track.artist_credit)"
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
|
@ -109,7 +110,7 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</p>
|
</p>
|
||||||
<p class="track-meta mobile">
|
<p class="track-meta mobile">
|
||||||
{{ track.artist?.name }}
|
{{ generateTrackCreditString(track) }}
|
||||||
<span class="middle middledot symbol" />
|
<span class="middle middledot symbol" />
|
||||||
<human-duration
|
<human-duration
|
||||||
v-if="track.uploads[0] && track.uploads[0].duration"
|
v-if="track.uploads[0] && track.uploads[0].duration"
|
||||||
|
|
|
@ -10,7 +10,7 @@ import { computed, ref } from 'vue'
|
||||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
import useReport from '~/composables/moderation/useReport'
|
import useReport from '~/composables/moderation/useReport'
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'update:show', value: boolean): void
|
(e: 'update:show', value: boolean): void
|
||||||
}
|
}
|
||||||
|
@ -64,17 +64,17 @@ const favoriteButton = computed(() => isFavorite.value
|
||||||
: t('components.audio.track.Modal.button.addToFavorites')
|
: t('components.audio.track.Modal.button.addToFavorites')
|
||||||
)
|
)
|
||||||
|
|
||||||
const trackDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const trackDetailsButton = computed(() => props.track.artist_credit?.[0].artist.content_category === 'podcast'
|
||||||
? t('components.audio.track.Modal.button.episodeDetails')
|
? t('components.audio.track.Modal.button.episodeDetails')
|
||||||
: t('components.audio.track.Modal.button.trackDetails')
|
: t('components.audio.track.Modal.button.trackDetails')
|
||||||
)
|
)
|
||||||
|
|
||||||
const albumDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const albumDetailsButton = computed(() => props.track.artist_credit?.[0].artist?.content_category === 'podcast'
|
||||||
? t('components.audio.track.Modal.button.seriesDetails')
|
? t('components.audio.track.Modal.button.seriesDetails')
|
||||||
: t('components.audio.track.Modal.button.albumDetails')
|
: t('components.audio.track.Modal.button.albumDetails')
|
||||||
)
|
)
|
||||||
|
|
||||||
const artistDetailsButton = computed(() => props.track.artist?.content_category === 'podcast'
|
const artistDetailsButton = computed(() => props.track.artist_credit?.[0].artist?.content_category === 'podcast'
|
||||||
? t('components.audio.track.Modal.button.channelDetails')
|
? t('components.audio.track.Modal.button.channelDetails')
|
||||||
: t('components.audio.track.Modal.button.artistDetails')
|
: t('components.audio.track.Modal.button.artistDetails')
|
||||||
)
|
)
|
||||||
|
@ -110,8 +110,8 @@ const labels = computed(() => ({
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="track.artist?.cover"
|
v-else-if="!!track.artist_credit.length && track.artist_credit[0].artist.cover"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](track.artist.cover.urls.medium_square_crop)"
|
v-lazy="getArtistCoverUrl(track.artist_credit)"
|
||||||
alt=""
|
alt=""
|
||||||
class="ui centered image"
|
class="ui centered image"
|
||||||
>
|
>
|
||||||
|
@ -126,14 +126,14 @@ const labels = computed(() => ({
|
||||||
{{ track.title }}
|
{{ track.title }}
|
||||||
</h3>
|
</h3>
|
||||||
<h4 class="track-modal-subtitle">
|
<h4 class="track-modal-subtitle">
|
||||||
{{ track.artist?.name }}
|
{{ generateTrackCreditString(track) }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider" />
|
<div class="ui hidden divider" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="ui one column unstackable grid">
|
<div class="ui one column unstackable grid">
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated && track.artist?.content_category !== 'podcast'"
|
v-if="$store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
@ -213,13 +213,16 @@ const labels = computed(() => ({
|
||||||
class="row"
|
class="row"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
v-for="ac in track.artist_credit"
|
||||||
|
:key="ac.artist.id"
|
||||||
class="column"
|
class="column"
|
||||||
role="button"
|
role="button"
|
||||||
:aria-label="artistDetailsButton"
|
:aria-label="artistDetailsButton"
|
||||||
@click.prevent.exact="$router.push({ name: 'library.artists.detail', params: { id: track.artist?.id } })"
|
@click.prevent.exact="$router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
|
||||||
>
|
>
|
||||||
<i class="user icon track-modal list-icon" />
|
<i class="user icon track-modal list-icon" />
|
||||||
<span class="track-modal list-item">{{ artistDetailsButton }}</span>
|
<span class="track-modal list-item">{{ ac.credit }}</span>
|
||||||
|
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -235,7 +238,7 @@ const labels = computed(() => ({
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider" />
|
<div class="ui divider" />
|
||||||
<div
|
<div
|
||||||
v-for="obj in getReportableObjects({ track, album: track.album, artist: track.artist })"
|
v-for="obj in getReportableObjects({ track, album: track.album, artistCredit: track.artist_credit })"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
class="row"
|
class="row"
|
||||||
@click.stop.prevent="report(obj)"
|
@click.stop.prevent="report(obj)"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
|
import type { Track, ArtistCredit, Album, Playlist, Library, Channel, Actor } from '~/types'
|
||||||
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
|
@ -26,7 +26,7 @@ interface Props extends PlayOptionsProps {
|
||||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||||
tracks: Track[]
|
tracks: Track[]
|
||||||
isPlayable?: boolean
|
isPlayable?: boolean
|
||||||
artist?: Artist | null
|
artistCredit?: ArtistCredit[] | null
|
||||||
album?: Album | null
|
album?: Album | null
|
||||||
playlist?: Playlist | null
|
playlist?: Playlist | null
|
||||||
library?: Library | null
|
library?: Library | null
|
||||||
|
@ -42,7 +42,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
showPosition: false,
|
showPosition: false,
|
||||||
displayActions: true,
|
displayActions: true,
|
||||||
|
|
||||||
artist: null,
|
artistCredit: null,
|
||||||
album: null,
|
album: null,
|
||||||
playlist: null,
|
playlist: null,
|
||||||
library: null,
|
library: null,
|
||||||
|
@ -130,8 +130,8 @@ const hover = ref(false)
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="track.artist?.cover?.urls.original"
|
v-else-if="track.artist_credit?.length && track.artist_credit[0].artist.cover?.urls.original"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](track.artist.cover.urls.medium_square_crop) "
|
v-lazy="$store.getters['instance/absoluteUrl'](track.artist_credit[0].artist.cover.urls.medium_square_crop) "
|
||||||
alt=""
|
alt=""
|
||||||
class="ui artist-track mini image"
|
class="ui artist-track mini image"
|
||||||
>
|
>
|
||||||
|
@ -166,15 +166,21 @@ const hover = ref(false)
|
||||||
v-if="showArtist"
|
v-if="showArtist"
|
||||||
class="content ellipsis left floated column"
|
class="content ellipsis left floated column"
|
||||||
>
|
>
|
||||||
<router-link
|
<template
|
||||||
class="artist link"
|
v-for="ac in track.artist_credit"
|
||||||
:to="{
|
:key="ac.artist.id"
|
||||||
name: 'library.artists.detail',
|
|
||||||
params: { id: track.artist?.id },
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
{{ track.artist?.name }}
|
<router-link
|
||||||
</router-link>
|
class="artist link"
|
||||||
|
:to="{
|
||||||
|
name: 'library.artists.detail',
|
||||||
|
params: { id: ac.artist?.id },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ ac.credit }}
|
||||||
|
</router-link>
|
||||||
|
<span>{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$store.state.auth.authenticated"
|
v-if="$store.state.auth.authenticated"
|
||||||
|
|
|
@ -13,6 +13,8 @@ import TagsList from '~/components/tags/List.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
import { getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'count', count: number): void
|
(e: 'count', count: number): void
|
||||||
}
|
}
|
||||||
|
@ -117,8 +119,8 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
alt=""
|
alt=""
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-else-if="object.track.artist?.cover"
|
v-else-if="object.track.artist_credit && object.track.artist_credit.length > 0"
|
||||||
v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"
|
v-lazy="getArtistCoverUrl(object.track.artist_credit)"
|
||||||
alt=""
|
alt=""
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
@ -142,16 +144,20 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="object.track.artist"
|
v-if="object.track.artist_credit"
|
||||||
class="meta ellipsis"
|
class="meta ellipsis"
|
||||||
>
|
>
|
||||||
<span>
|
<span
|
||||||
|
v-for="ac in object.track.artist_credit"
|
||||||
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
<router-link
|
<router-link
|
||||||
class="discrete link"
|
class="discrete link"
|
||||||
:to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}"
|
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
|
||||||
>
|
>
|
||||||
{{ object.track.artist.name }}
|
{{ ac.credit }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<tags-list
|
<tags-list
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Track, Album, Artist, Library } from '~/types'
|
import type { Track, Album, Artist, Library, ArtistCredit } from '~/types'
|
||||||
|
|
||||||
import { momentFormat } from '~/utils/filters'
|
import { momentFormat } from '~/utils/filters'
|
||||||
import { computed, reactive, ref, watch } from 'vue'
|
import { computed, reactive, ref, watch } from 'vue'
|
||||||
|
@ -9,7 +9,7 @@ import { sum } from 'lodash-es'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import ArtistLabel from '~/components/audio/ArtistLabel.vue'
|
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
|
||||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||||
import TagsList from '~/components/tags/List.vue'
|
import TagsList from '~/components/tags/List.vue'
|
||||||
import AlbumDropdown from './AlbumDropdown.vue'
|
import AlbumDropdown from './AlbumDropdown.vue'
|
||||||
|
@ -30,13 +30,21 @@ const props = defineProps<Props>()
|
||||||
|
|
||||||
const object = ref<Album | null>(null)
|
const object = ref<Album | null>(null)
|
||||||
const artist = ref<Artist | null>(null)
|
const artist = ref<Artist | null>(null)
|
||||||
|
const artistCredit = ref([] as ArtistCredit[])
|
||||||
const libraries = ref([] as Library[])
|
const libraries = ref([] as Library[])
|
||||||
const paginateBy = ref(50)
|
const paginateBy = ref(50)
|
||||||
|
|
||||||
const totalTracks = computed(() => object.value?.tracks_count ?? 0)
|
const totalTracks = computed(() => object.value?.tracks_count ?? 0)
|
||||||
const isChannel = computed(() => !!object.value?.artist.channel)
|
const isChannel = computed(() => {
|
||||||
const isAlbum = computed(() => object.value?.artist.content_category === 'music')
|
return artistCredit.value.some(ac => ac.artist.channel)
|
||||||
const isSerie = computed(() => object.value?.artist.content_category === 'podcast')
|
})
|
||||||
|
const isAlbum = computed(() => {
|
||||||
|
return artistCredit.value.some(ac => ac.artist.content_category === 'music')
|
||||||
|
})
|
||||||
|
const isSerie = computed(() => {
|
||||||
|
return artistCredit.value.some(ac => ac.artist.content_category === 'podcast')
|
||||||
|
})
|
||||||
|
|
||||||
const totalDuration = computed(() => sum((object.value?.tracks ?? []).map(track => track.uploads[0]?.duration ?? 0)))
|
const totalDuration = computed(() => sum((object.value?.tracks ?? []).map(track => track.uploads[0]?.duration ?? 0)))
|
||||||
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||||
|
|
||||||
|
@ -52,7 +60,10 @@ const fetchData = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
const albumResponse = await axios.get(`albums/${props.id}/`, { params: { refresh: 'true' } })
|
const albumResponse = await axios.get(`albums/${props.id}/`, { params: { refresh: 'true' } })
|
||||||
const artistResponse = await axios.get(`artists/${albumResponse.data.artist.id}/`)
|
|
||||||
|
artistCredit.value = albumResponse.data.artist_credit
|
||||||
|
|
||||||
|
const artistResponse = await axios.get(`artists/${albumResponse.data.artist_credit[0].artist.id}/`)
|
||||||
|
|
||||||
artist.value = artistResponse.data
|
artist.value = artistResponse.data
|
||||||
if (artist.value?.channel) {
|
if (artist.value?.channel) {
|
||||||
|
@ -201,7 +212,7 @@ const remove = async () => {
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:is-channel="isChannel"
|
:is-channel="isChannel"
|
||||||
:artist="artist"
|
:artist-credit="artistCredit"
|
||||||
@remove="remove"
|
@remove="remove"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -214,9 +225,9 @@ const remove = async () => {
|
||||||
>
|
>
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label
|
<artist-credit-label
|
||||||
v-if="artist"
|
v-if="artistCredit"
|
||||||
:artist="artist"
|
:artist-credit="artistCredit"
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
|
@ -244,10 +255,9 @@ const remove = async () => {
|
||||||
>
|
>
|
||||||
{{ object.title }}
|
{{ object.title }}
|
||||||
</h2>
|
</h2>
|
||||||
<artist-label
|
<artist-credit-label
|
||||||
v-if="artist"
|
v-if="artistCredit"
|
||||||
:artist="artist"
|
:artist-credit="artistCredit"
|
||||||
class="rounded"
|
|
||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div
|
||||||
|
@ -285,7 +295,7 @@ const remove = async () => {
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:is-channel="isChannel"
|
:is-channel="isChannel"
|
||||||
:artist="artist"
|
:artist-credit="artistCredit"
|
||||||
@remove="remove"
|
@remove="remove"
|
||||||
/>
|
/>
|
||||||
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
|
<div v-if="(object.tags && object.tags.length > 0) || object.description || $store.state.auth.authenticated && object.is_local">
|
||||||
|
@ -333,7 +343,7 @@ const remove = async () => {
|
||||||
:paginate-by="paginateBy"
|
:paginate-by="paginateBy"
|
||||||
:total-tracks="totalTracks"
|
:total-tracks="totalTracks"
|
||||||
:is-serie="isSerie"
|
:is-serie="isSerie"
|
||||||
:artist="artist"
|
:artist-credit="artistCredit"
|
||||||
:object="object"
|
:object="object"
|
||||||
:is-loading-tracks="isLoadingTracks"
|
:is-loading-tracks="isLoadingTracks"
|
||||||
object-type="album"
|
object-type="album"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Artist, Album, Library, Track } from '~/types'
|
import type { ArtistCredit, Album, Library, Track } from '~/types'
|
||||||
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
|
import ChannelEntries from '~/components/audio/ChannelEntries.vue'
|
||||||
|
@ -17,7 +17,7 @@ interface Props {
|
||||||
|
|
||||||
isLoadingTracks: boolean
|
isLoadingTracks: boolean
|
||||||
isSerie: boolean
|
isSerie: boolean
|
||||||
artist: Artist
|
artistCredit: ArtistCredit[]
|
||||||
paginateBy: number
|
paginateBy: number
|
||||||
totalTracks: number
|
totalTracks: number
|
||||||
}
|
}
|
||||||
|
@ -66,10 +66,10 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<channel-entries
|
<channel-entries
|
||||||
v-if="artist.channel && isSerie"
|
v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
|
||||||
:is-podcast="isSerie"
|
:is-podcast="isSerie"
|
||||||
:limit="50"
|
:limit="50"
|
||||||
:filters="{channel: artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
:filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
@ -123,7 +123,7 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="!artist.channel && !isSerie">
|
<template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie">
|
||||||
<h2>
|
<h2>
|
||||||
{{ $t('components.library.AlbumDetail.header.libraries') }}
|
{{ $t('components.library.AlbumDetail.header.libraries') }}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Album, Artist, Library } from '~/types'
|
import type { Album, ArtistCredit, Library } from '~/types'
|
||||||
|
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
@ -17,7 +17,7 @@ interface Events {
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isLoading: boolean
|
isLoading: boolean
|
||||||
artist: Artist | null
|
artistCredit: ArtistCredit[]
|
||||||
object: Album
|
object: Album
|
||||||
publicLibraries: Library[]
|
publicLibraries: Library[]
|
||||||
isAlbum: boolean
|
isAlbum: boolean
|
||||||
|
@ -38,9 +38,9 @@ const labels = computed(() => ({
|
||||||
more: t('components.library.AlbumDropdown.button.more')
|
more: t('components.library.AlbumDropdown.button.more')
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const isEmbedable = computed(() => (props.isChannel && props.artist?.channel?.actor) || props.publicLibraries.length)
|
const isEmbedable = computed(() => (props.isChannel && props.artistCredit[0].artist?.channel?.actor) || props.publicLibraries.length)
|
||||||
const musicbrainzUrl = computed(() => props.object?.mbid ? `https://musicbrainz.org/release/${props.object.mbid}` : null)
|
const musicbrainzUrl = computed(() => props.object?.mbid ? `https://musicbrainz.org/release/${props.object.mbid}` : null)
|
||||||
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(props.object?.title)}&artist=${encodeURI(props.object?.artist.name)}`)
|
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(props.object?.title)}&artist=${encodeURI(props.object?.artist_credit[0].artist.name)}`)
|
||||||
|
|
||||||
const remove = () => emit('remove')
|
const remove = () => emit('remove')
|
||||||
</script>
|
</script>
|
||||||
|
@ -125,7 +125,7 @@ const remove = () => emit('remove')
|
||||||
{{ $t('components.library.AlbumDropdown.button.edit') }}
|
{{ $t('components.library.AlbumDropdown.button.edit') }}
|
||||||
</router-link>
|
</router-link>
|
||||||
<dangerous-button
|
<dangerous-button
|
||||||
v-if="artist && $store.state.auth.authenticated && artist.channel && artist.attributed_to.full_username === $store.state.auth.fullUsername"
|
v-if="artistCredit[0] && $store.state.auth.authenticated && artistCredit[0].artist.channel && artistCredit[0].artist.attributed_to?.full_username === $store.state.auth.fullUsername"
|
||||||
:class="['ui', {loading: isLoading}, 'item']"
|
:class="['ui', {loading: isLoading}, 'item']"
|
||||||
@confirm="remove()"
|
@confirm="remove()"
|
||||||
>
|
>
|
||||||
|
@ -151,7 +151,7 @@ const remove = () => emit('remove')
|
||||||
</dangerous-button>
|
</dangerous-button>
|
||||||
<div class="divider" />
|
<div class="divider" />
|
||||||
<div
|
<div
|
||||||
v-for="obj in getReportableObjects({album: object, channel: artist?.channel})"
|
v-for="obj in getReportableObjects({album: object, channel: artistCredit[0]?.artist.channel})"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
role="button"
|
role="button"
|
||||||
class="basic item"
|
class="basic item"
|
||||||
|
|
|
@ -47,8 +47,8 @@ const domain = computed(() => getDomain(track.value?.fid ?? ''))
|
||||||
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
|
||||||
const isEmbedable = computed(() => artist.value?.channel?.actor || publicLibraries.value.length)
|
const isEmbedable = computed(() => artist.value?.channel?.actor || publicLibraries.value.length)
|
||||||
const upload = computed(() => track.value?.uploads?.[0] ?? null)
|
const upload = computed(() => track.value?.uploads?.[0] ?? null)
|
||||||
const wikipediaUrl = computed(() => `https://en.wikipedia.org/w/index.php?search=${encodeURI(`${track.value?.title ?? ''} ${track.value?.artist?.name ?? ''}`)}`)
|
const wikipediaUrl = computed(() => `https://en.wikipedia.org/w/index.php?search=${encodeURI(`${track.value?.title ?? ''} ${track.value?.artist_credit?.[0].artist?.name ?? ''}`)}`)
|
||||||
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(track.value?.album?.title ?? '')}&artist=${encodeURI(track.value?.artist?.name ?? '')}&title=${encodeURI(track.value?.title ?? '')}`)
|
const discogsUrl = computed(() => `https://discogs.com/search/?type=release&title=${encodeURI(track.value?.album?.title ?? '')}&artist=${encodeURI(track.value?.artist_credit?.[0].artist?.name ?? '')}&title=${encodeURI(track.value?.title ?? '')}`)
|
||||||
const downloadUrl = computed(() => {
|
const downloadUrl = computed(() => {
|
||||||
const url = store.getters['instance/absoluteUrl'](upload.value?.listen_url ?? '')
|
const url = store.getters['instance/absoluteUrl'](upload.value?.listen_url ?? '')
|
||||||
return store.state.auth.authenticated
|
return store.state.auth.authenticated
|
||||||
|
|
|
@ -76,7 +76,7 @@ watchEffect(() => {
|
||||||
class="ui fluid image track-cover-image"
|
class="ui fluid image track-cover-image"
|
||||||
>
|
>
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<span v-if="track.artist?.content_category === 'music'">
|
<span v-if="track.artist_credit?.[0].artist?.content_category === 'music'">
|
||||||
{{ $t('components.library.TrackDetail.header.track') }}
|
{{ $t('components.library.TrackDetail.header.track') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
@ -169,14 +169,24 @@ watchEffect(() => {
|
||||||
{{ $t('components.library.TrackDetail.table.release.artist') }}
|
{{ $t('components.library.TrackDetail.table.release.artist') }}
|
||||||
</td>
|
</td>
|
||||||
<td class="right aligned">
|
<td class="right aligned">
|
||||||
<router-link :to="{name: 'library.artists.detail', params: {id: track.artist?.id}}">
|
<template
|
||||||
{{ track.artist?.name }}
|
v-for="ac in track.artist_credit"
|
||||||
</router-link>
|
:key="ac.artist.id"
|
||||||
|
>
|
||||||
|
<router-link
|
||||||
|
class="discrete link"
|
||||||
|
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}"
|
||||||
|
style="display: inline;"
|
||||||
|
>
|
||||||
|
{{ ac.credit }}
|
||||||
|
</router-link>
|
||||||
|
<span style="display: inline;">{{ ac.joinphrase }}</span>
|
||||||
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="track.album">
|
<tr v-if="track.album">
|
||||||
<td>
|
<td>
|
||||||
<span v-if="track.album.artist.content_category === 'music'">
|
<span v-if="track.album.artist_credit?.[0].artist.content_category === 'music'">
|
||||||
{{ $t('components.library.TrackDetail.table.release.album') }}
|
{{ $t('components.library.TrackDetail.table.release.album') }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue