Drop libraries in favor of playlist for user audio sharing (#2366)
This commit is contained in:
parent
7102da8ed3
commit
c9d915fb33
|
@ -49,6 +49,7 @@ def handler_create_user(
|
|||
utils.logger.warn("Unknown permission %s", permission)
|
||||
utils.logger.debug("Creating actor…")
|
||||
user.actor = models.create_actor(user)
|
||||
models.create_user_libraries(user)
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
|
|
@ -627,12 +627,6 @@ def get_actors_from_audience(urls):
|
|||
final_query, Q(pk__in=actor_follows.values_list("actor", flat=True))
|
||||
)
|
||||
|
||||
library_follows = models.LibraryFollow.objects.filter(
|
||||
queries["followed"], approved=True
|
||||
)
|
||||
final_query = funkwhale_utils.join_queries_or(
|
||||
final_query, Q(pk__in=library_follows.values_list("actor", flat=True))
|
||||
)
|
||||
if not final_query:
|
||||
return models.Actor.objects.none()
|
||||
return models.Actor.objects.filter(final_query)
|
||||
|
|
|
@ -56,7 +56,6 @@ class LibrarySerializer(serializers.ModelSerializer):
|
|||
"uuid",
|
||||
"actor",
|
||||
"name",
|
||||
"description",
|
||||
"creation_date",
|
||||
"uploads_count",
|
||||
"privacy_level",
|
||||
|
|
|
@ -173,13 +173,9 @@ class MusicLibraryFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
|||
uuid = factory.Faker("uuid4")
|
||||
actor = factory.SubFactory(ActorFactory)
|
||||
privacy_level = "me"
|
||||
name = factory.Faker("sentence")
|
||||
description = factory.Faker("sentence")
|
||||
name = privacy_level
|
||||
uploads_count = 0
|
||||
fid = factory.Faker("federation_url")
|
||||
followers_url = factory.LazyAttribute(
|
||||
lambda o: o.fid + "/followers" if o.fid else None
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = "music.Library"
|
||||
|
|
|
@ -9,7 +9,7 @@ MODELS = [
|
|||
(music_models.Album, ["fid"]),
|
||||
(music_models.Track, ["fid"]),
|
||||
(music_models.Upload, ["fid"]),
|
||||
(music_models.Library, ["fid", "followers_url"]),
|
||||
(music_models.Library, ["fid"]),
|
||||
(
|
||||
federation_models.Actor,
|
||||
[
|
||||
|
|
|
@ -166,7 +166,7 @@ def outbox_follow(context):
|
|||
def outbox_create_audio(context):
|
||||
upload = context["upload"]
|
||||
channel = upload.library.get_channel()
|
||||
followers_target = channel.actor if channel else upload.library
|
||||
followers_target = channel.actor if channel else upload.library.actor
|
||||
actor = channel.actor if channel else upload.library.actor
|
||||
if channel:
|
||||
serializer = serializers.ChannelCreateUploadSerializer(upload)
|
||||
|
@ -310,8 +310,8 @@ def outbox_delete_audio(context):
|
|||
uploads = context["uploads"]
|
||||
library = uploads[0].library
|
||||
channel = library.get_channel()
|
||||
followers_target = channel.actor if channel else library
|
||||
actor = channel.actor if channel else library.actor
|
||||
followers_target = channel.actor if channel else actor
|
||||
serializer = serializers.ActivitySerializer(
|
||||
{
|
||||
"type": "Delete",
|
||||
|
@ -679,6 +679,9 @@ def inbox_delete_favorite(payload, context):
|
|||
favorite.delete()
|
||||
|
||||
|
||||
# to do : test listening routes and broadcast
|
||||
|
||||
|
||||
@outbox.register({"type": "Listen", "object.type": "Track"})
|
||||
def outbox_create_listening(context):
|
||||
track = context["track"]
|
||||
|
|
|
@ -995,8 +995,6 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
actor = serializers.URLField(max_length=500, required=False)
|
||||
attributedTo = serializers.URLField(max_length=500, required=False)
|
||||
name = serializers.CharField()
|
||||
summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
|
||||
followers = serializers.URLField(max_length=500)
|
||||
audience = serializers.ChoiceField(
|
||||
choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
|
||||
required=False,
|
||||
|
@ -1013,9 +1011,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
PAGINATED_COLLECTION_JSONLD_MAPPING,
|
||||
{
|
||||
"name": jsonld.first_val(contexts.AS.name),
|
||||
"summary": jsonld.first_val(contexts.AS.summary),
|
||||
"audience": jsonld.first_id(contexts.AS.audience),
|
||||
"followers": jsonld.first_id(contexts.AS.followers),
|
||||
"actor": jsonld.first_id(contexts.AS.actor),
|
||||
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
|
||||
},
|
||||
|
@ -1037,7 +1033,6 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
conf = {
|
||||
"id": library.fid,
|
||||
"name": library.name,
|
||||
"summary": library.description,
|
||||
"page_size": 100,
|
||||
"attributedTo": library.actor,
|
||||
"actor": library.actor,
|
||||
|
@ -1048,7 +1043,6 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
r["audience"] = (
|
||||
contexts.AS.Public if library.privacy_level == "everyone" else ""
|
||||
)
|
||||
r["followers"] = library.followers_url
|
||||
return r
|
||||
|
||||
def create(self, validated_data):
|
||||
|
@ -1068,8 +1062,6 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
|||
defaults={
|
||||
"uploads_count": validated_data["totalItems"],
|
||||
"name": validated_data["name"],
|
||||
"description": validated_data.get("summary"),
|
||||
"followers_url": validated_data["followers"],
|
||||
"privacy_level": privacy[validated_data["audience"]],
|
||||
},
|
||||
)
|
||||
|
|
|
@ -387,7 +387,6 @@ class MusicLibraryViewSet(
|
|||
"id": lb.get_federation_id(),
|
||||
"actor": lb.actor,
|
||||
"name": lb.name,
|
||||
"summary": lb.description,
|
||||
"items": lb.uploads.for_federation()
|
||||
.order_by("-creation_date")
|
||||
.prefetch_related(
|
||||
|
|
|
@ -572,7 +572,6 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
|||
domain = serializers.CharField(source="domain_name")
|
||||
actor = ManageBaseActorSerializer()
|
||||
uploads_count = serializers.SerializerMethodField()
|
||||
followers_count = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = music_models.Library
|
||||
|
@ -582,14 +581,11 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
|||
"fid",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"domain",
|
||||
"is_local",
|
||||
"creation_date",
|
||||
"privacy_level",
|
||||
"uploads_count",
|
||||
"followers_count",
|
||||
"followers_url",
|
||||
"actor",
|
||||
]
|
||||
read_only_fields = [
|
||||
|
@ -605,10 +601,6 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
|||
def get_uploads_count(self, obj) -> int:
|
||||
return getattr(obj, "_uploads_count", int(obj.uploads_count))
|
||||
|
||||
@extend_schema_field(OpenApiTypes.INT)
|
||||
def get_followers_count(self, obj):
|
||||
return getattr(obj, "followers_count", None)
|
||||
|
||||
|
||||
class ManageNestedLibrarySerializer(serializers.ModelSerializer):
|
||||
domain = serializers.CharField(source="domain_name")
|
||||
|
@ -622,12 +614,10 @@ class ManageNestedLibrarySerializer(serializers.ModelSerializer):
|
|||
"fid",
|
||||
"url",
|
||||
"name",
|
||||
"description",
|
||||
"domain",
|
||||
"is_local",
|
||||
"creation_date",
|
||||
"privacy_level",
|
||||
"followers_url",
|
||||
"actor",
|
||||
]
|
||||
|
||||
|
|
|
@ -164,7 +164,6 @@ class LibraryStateSerializer(serializers.ModelSerializer):
|
|||
"uuid",
|
||||
"fid",
|
||||
"name",
|
||||
"description",
|
||||
"creation_date",
|
||||
"privacy_level",
|
||||
]
|
||||
|
|
|
@ -10,7 +10,7 @@ from funkwhale_api.federation import factories as federation_factories
|
|||
from funkwhale_api.history import factories as history_factories
|
||||
from funkwhale_api.music import factories as music_factories
|
||||
from funkwhale_api.playlists import factories as playlist_factories
|
||||
from funkwhale_api.users import serializers
|
||||
from funkwhale_api.users import models, serializers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -37,6 +37,7 @@ def create_data(count=2, super_user_name=None):
|
|||
print(
|
||||
f"Superuser {super_user_name} already in db. Skipping fake-data creation"
|
||||
)
|
||||
super_user = models.User.objects.get(username=super_user_name)
|
||||
continue
|
||||
else:
|
||||
raise e
|
||||
|
@ -68,6 +69,9 @@ def create_data(count=2, super_user_name=None):
|
|||
),
|
||||
)
|
||||
playlist_factories.PlaylistTrackFactory(playlist=playlist, track=upload.track)
|
||||
federation_factories.LibraryFollowFactory.create_batch(
|
||||
size=random.randint(3, 18), actor=super_user.actor
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -36,6 +36,7 @@ def set_all_artists_credit(apps, schema_editor):
|
|||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("music", "0058_upload_quality"),
|
||||
("playlists", "0008_playlist_library_drop"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# Generated by Django 4.2.9 on 2025-01-03 20:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("music", "0059_remove_album_artist_remove_track_artist_artistcredit_and_more"),
|
||||
("playlists", "0007_alter_playlist_actor_alter_playlisttrack_uuid_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="library",
|
||||
name="description",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="library",
|
||||
name="followers_url",
|
||||
),
|
||||
]
|
|
@ -0,0 +1,108 @@
|
|||
# Generated by Django 4.2.9 on 2025-01-03 16:12
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.db import IntegrityError
|
||||
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from django.urls import reverse
|
||||
import uuid
|
||||
|
||||
|
||||
def insert_tracks_to_playlist(apps, playlist, uploads):
|
||||
PlaylistTrack = apps.get_model("playlists", "PlaylistTrack")
|
||||
plts = [
|
||||
PlaylistTrack(
|
||||
creation_date=playlist.creation_date,
|
||||
playlist=playlist,
|
||||
track=upload.track,
|
||||
index=0 + i,
|
||||
uuid=(new_uuid := uuid.uuid4()),
|
||||
fid=federation_utils.full_url(
|
||||
reverse(
|
||||
f"federation:music:playlists-detail",
|
||||
kwargs={"uuid": new_uuid},
|
||||
)
|
||||
),
|
||||
)
|
||||
for i, upload in enumerate(uploads)
|
||||
if upload.track
|
||||
]
|
||||
|
||||
return PlaylistTrack.objects.bulk_create(plts)
|
||||
|
||||
|
||||
def migrate_libraries_to_playlist(apps, schema_editor):
|
||||
Playlist = apps.get_model("playlists", "Playlist")
|
||||
Library = apps.get_model("music", "Library")
|
||||
LibraryFollow = apps.get_model("federation", "LibraryFollow")
|
||||
Follow = apps.get_model("federation", "Follow")
|
||||
User = apps.get_model("users", "User")
|
||||
Actor = apps.get_model("federation", "Actor")
|
||||
|
||||
# library to playlist
|
||||
for library in Library.objects.all():
|
||||
playlist = Playlist.objects.create(
|
||||
name=library.name,
|
||||
actor=library.actor,
|
||||
creation_date=library.creation_date,
|
||||
privacy_level=library.privacy_level,
|
||||
uuid=(new_uuid := uuid.uuid4()),
|
||||
fid=federation_utils.full_url(
|
||||
reverse(
|
||||
f"federation:music:playlists-detail",
|
||||
kwargs={"uuid": new_uuid},
|
||||
)
|
||||
),
|
||||
)
|
||||
playlist.save()
|
||||
|
||||
if library.uploads.all().exists():
|
||||
insert_tracks_to_playlist(apps, playlist, library.uploads.all())
|
||||
|
||||
# library follows to user follow
|
||||
for lib_follow in LibraryFollow.objects.filter(target=library):
|
||||
try:
|
||||
Follow.objects.create(
|
||||
uuid=lib_follow.uuid,
|
||||
target=library.actor,
|
||||
actor=lib_follow.actor,
|
||||
approved=lib_follow.approved,
|
||||
creation_date=lib_follow.creation_date,
|
||||
modification_date=lib_follow.modification_date,
|
||||
)
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
LibraryFollow.objects.all().delete()
|
||||
|
||||
# migrate uploads to new library
|
||||
for actor in Actor.objects.all():
|
||||
privacy_levels = ["me", "instance", "everyone"]
|
||||
for privacy_level in privacy_levels:
|
||||
build_in_lib = Library.objects.create(
|
||||
actor=actor,
|
||||
privacy_level=privacy_level,
|
||||
name=privacy_level,
|
||||
uuid=(new_uuid := uuid.uuid4()),
|
||||
fid=federation_utils.full_url(
|
||||
reverse(
|
||||
f"federation:music:playlists-detail",
|
||||
kwargs={"uuid": new_uuid},
|
||||
)
|
||||
),
|
||||
)
|
||||
for library in actor.libraries.filter(privacy_level=privacy_level):
|
||||
library.uploads.all().update(library=build_in_lib)
|
||||
if library.pk is not build_in_lib.pk:
|
||||
library.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("music", "0060_remove_library_description_and_more"),
|
||||
]
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_libraries_to_playlist, reverse_code=migrations.RunPython.noop
|
||||
),
|
||||
]
|
|
@ -1319,10 +1319,8 @@ class Library(federation_models.FederationMixin):
|
|||
actor = models.ForeignKey(
|
||||
"federation.Actor", related_name="libraries", on_delete=models.CASCADE
|
||||
)
|
||||
followers_url = models.URLField(max_length=500)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
name = models.CharField(max_length=100)
|
||||
description = models.TextField(max_length=5000, null=True, blank=True)
|
||||
privacy_level = models.CharField(
|
||||
choices=LIBRARY_PRIVACY_LEVEL_CHOICES, default="me", max_length=25
|
||||
)
|
||||
|
|
|
@ -329,7 +329,6 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer):
|
|||
"uuid",
|
||||
"fid",
|
||||
"name",
|
||||
"description",
|
||||
"privacy_level",
|
||||
"uploads_count",
|
||||
"size",
|
||||
|
@ -528,6 +527,26 @@ class UploadForOwnerSerializer(UploadSerializer):
|
|||
return f
|
||||
|
||||
|
||||
class UploadBulkUpdateSerializer(serializers.Serializer):
|
||||
uuid = serializers.UUIDField()
|
||||
privacy_level = serializers.ChoiceField(
|
||||
choices=models.LIBRARY_PRIVACY_LEVEL_CHOICES
|
||||
)
|
||||
|
||||
def validate(self, data):
|
||||
try:
|
||||
upload = models.Upload.objects.get(uuid=data["uuid"])
|
||||
except models.Upload.DoesNotExist:
|
||||
raise serializers.ValidationError(
|
||||
f"Upload with uuid {data['uuid']} does not exist"
|
||||
)
|
||||
|
||||
upload.library = upload.library.actor.libraries.get(
|
||||
privacy_level=data["privacy_level"]
|
||||
)
|
||||
return upload
|
||||
|
||||
|
||||
class UploadActionSerializer(common_serializers.ActionSerializer):
|
||||
actions = [
|
||||
common_serializers.Action("delete", allow_all=True),
|
||||
|
|
|
@ -340,7 +340,6 @@ def library_library(request, uuid, redirect_to_ap):
|
|||
{"tag": "meta", "property": "og:url", "content": library_url},
|
||||
{"tag": "meta", "property": "og:type", "content": "website"},
|
||||
{"tag": "meta", "property": "og:title", "content": obj.name},
|
||||
{"tag": "meta", "property": "og:description", "content": obj.description},
|
||||
]
|
||||
|
||||
if preferences.get("federation__enabled"):
|
||||
|
|
|
@ -293,11 +293,8 @@ class AlbumViewSet(
|
|||
|
||||
|
||||
class LibraryViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
viewsets.GenericViewSet,
|
||||
):
|
||||
lookup_field = "uuid"
|
||||
|
@ -332,42 +329,6 @@ class LibraryViewSet(
|
|||
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(actor=self.request.user.actor)
|
||||
|
||||
@transaction.atomic
|
||||
def perform_destroy(self, instance):
|
||||
routes.outbox.dispatch(
|
||||
{"type": "Delete", "object": {"type": "Library"}},
|
||||
context={"library": instance},
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@extend_schema(
|
||||
responses=federation_api_serializers.LibraryFollowSerializer(many=True)
|
||||
)
|
||||
@action(
|
||||
methods=["get"],
|
||||
detail=True,
|
||||
)
|
||||
@transaction.non_atomic_requests
|
||||
def follows(self, request, *args, **kwargs):
|
||||
library = self.get_object()
|
||||
queryset = (
|
||||
library.received_follows.filter(target__actor=self.request.user.actor)
|
||||
.prefetch_related("actor", "target__actor")
|
||||
.order_by("-creation_date")
|
||||
)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = federation_api_serializers.LibraryFollowSerializer(
|
||||
page, many=True, required=False
|
||||
)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True, required=False)
|
||||
return Response(serializer.data)
|
||||
|
||||
# TODO quickfix, basically specifying the response would be None
|
||||
@extend_schema(responses=None)
|
||||
@action(
|
||||
|
@ -830,7 +791,7 @@ class UploadViewSet(
|
|||
return Response(payload, status=200)
|
||||
|
||||
@action(methods=["post"], detail=False)
|
||||
def action(self, request, *args, **kwargs):
|
||||
def perform_upload_action(self, request, *args, **kwargs):
|
||||
queryset = self.get_queryset()
|
||||
serializer = serializers.UploadActionSerializer(request.data, queryset=queryset)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
@ -860,6 +821,23 @@ class UploadViewSet(
|
|||
)
|
||||
instance.delete()
|
||||
|
||||
@action(detail=False, methods=["patch"])
|
||||
def bulk_update(self, request, *args, **kwargs):
|
||||
"""
|
||||
Used to move an upload from one library to another. Receive a upload uuid and a privacy_level
|
||||
"""
|
||||
serializer = serializers.UploadBulkUpdateSerializer(
|
||||
data=request.data, many=True
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
models.Upload.objects.bulk_update(serializer.validated_data, ["library"])
|
||||
|
||||
return Response(
|
||||
serializers.UploadForOwnerSerializer(serializer.validated_data).data,
|
||||
status=200,
|
||||
)
|
||||
|
||||
|
||||
class Search(views.APIView):
|
||||
max_results = 3
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Generated by Django 4.2.9 on 2025-01-03 16:12
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("playlists", "0007_alter_playlist_actor_alter_playlisttrack_uuid_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="playlist",
|
||||
name="name",
|
||||
field=models.CharField(max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="playlist",
|
||||
name="description",
|
||||
field=models.TextField(blank=True, max_length=5000, null=True),
|
||||
),
|
||||
]
|
|
@ -78,14 +78,14 @@ class PlaylistQuerySet(models.QuerySet, common_models.LocalFromFidQuerySet):
|
|||
|
||||
class Playlist(federation_models.FederationMixin):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||
name = models.CharField(max_length=50)
|
||||
name = models.CharField(max_length=100)
|
||||
actor = models.ForeignKey(
|
||||
"federation.Actor", related_name="playlists", on_delete=models.CASCADE
|
||||
)
|
||||
creation_date = models.DateTimeField(default=timezone.now)
|
||||
modification_date = models.DateTimeField(auto_now=True)
|
||||
privacy_level = fields.get_privacy_field()
|
||||
|
||||
description = models.TextField(max_length=5000, null=True, blank=True)
|
||||
objects = PlaylistQuerySet.as_manager()
|
||||
federation_namespace = "playlists"
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ from funkwhale_api.common import validators as common_validators
|
|||
from funkwhale_api.federation import keys
|
||||
from funkwhale_api.federation import models as federation_models
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.music import models as music_models
|
||||
|
||||
|
||||
def get_token(length=5):
|
||||
|
@ -457,6 +458,22 @@ def create_actor(user, **kwargs):
|
|||
return federation_models.Actor.objects.create(user=user, **args)
|
||||
|
||||
|
||||
def create_user_libraries(user):
|
||||
for privacy_level, l in music_models.LIBRARY_PRIVACY_LEVEL_CHOICES:
|
||||
music_models.Library.objects.create(
|
||||
actor=user.actor,
|
||||
privacy_level=privacy_level,
|
||||
name=privacy_level,
|
||||
uuid=(new_uuid := uuid.uuid4()),
|
||||
fid=federation_utils.full_url(
|
||||
reverse(
|
||||
"federation:music:playlists-detail",
|
||||
kwargs={"uuid": new_uuid},
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@receiver(ldap_populate_user)
|
||||
def init_ldap_user(sender, user, ldap_user, **kwargs):
|
||||
if not user.actor:
|
||||
|
|
|
@ -114,7 +114,7 @@ class RegisterSerializer(RS):
|
|||
user_request_id=user_request.pk,
|
||||
new_status=user_request.status,
|
||||
)
|
||||
|
||||
models.create_user_libraries(user)
|
||||
return user
|
||||
|
||||
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import uuid
|
||||
|
||||
import pytest
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
@ -246,9 +244,6 @@ def test_should_reject(factories, params, policy_kwargs, expected):
|
|||
|
||||
def test_get_actors_from_audience_urls(settings, db):
|
||||
settings.FEDERATION_HOSTNAME = "federation.hostname"
|
||||
library_uuid1 = uuid.uuid4()
|
||||
library_uuid2 = uuid.uuid4()
|
||||
|
||||
urls = [
|
||||
"https://wrong.url",
|
||||
"https://federation.hostname"
|
||||
|
@ -257,21 +252,15 @@ def test_get_actors_from_audience_urls(settings, db):
|
|||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}),
|
||||
"https://federation.hostname"
|
||||
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}),
|
||||
"https://federation.hostname",
|
||||
activity.PUBLIC_ADDRESS,
|
||||
]
|
||||
followed_query = Q(target__followers_url=urls[0])
|
||||
for url in urls[1:-1]:
|
||||
followed_query |= Q(target__followers_url=url)
|
||||
actor_follows = models.Follow.objects.filter(followed_query, approved=True)
|
||||
library_follows = models.LibraryFollow.objects.filter(followed_query, approved=True)
|
||||
expected = models.Actor.objects.filter(
|
||||
Q(fid__in=urls[0:-1])
|
||||
| Q(pk__in=actor_follows.values_list("actor", flat=True))
|
||||
| Q(pk__in=library_follows.values_list("actor", flat=True))
|
||||
Q(fid__in=urls[0:-1]) | Q(pk__in=actor_follows.values_list("actor", flat=True))
|
||||
)
|
||||
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
|
||||
|
||||
|
@ -478,17 +467,9 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
)
|
||||
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
|
||||
remote_actor4 = factories["federation.Actor"]()
|
||||
|
||||
library = factories["music.Library"]()
|
||||
library_follower_local = factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=True, approved=True
|
||||
).actor
|
||||
library_follower_remote = factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=False, approved=True
|
||||
).actor
|
||||
# follow not approved
|
||||
factories["federation.LibraryFollow"](
|
||||
target=library, actor__local=False, approved=False
|
||||
factories["federation.Follow"](
|
||||
target=remote_actor3, actor__local=False, approved=False
|
||||
)
|
||||
|
||||
followed_actor = factories["federation.Actor"]()
|
||||
|
@ -511,7 +492,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
remote_actor2,
|
||||
remote_actor3,
|
||||
activity.PUBLIC_ADDRESS,
|
||||
{"type": "followers", "target": library},
|
||||
{"type": "followers", "target": followed_actor},
|
||||
{"type": "actor_inbox", "actor": remote_actor4},
|
||||
]
|
||||
|
@ -524,7 +504,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
models.InboxItem(actor=local_actor1, type="to"),
|
||||
models.InboxItem(actor=local_actor2, type="to"),
|
||||
models.InboxItem(actor=local_actor3, type="to"),
|
||||
models.InboxItem(actor=library_follower_local, type="to"),
|
||||
models.InboxItem(actor=actor_follower_local, type="to"),
|
||||
],
|
||||
key=lambda v: v.actor.pk,
|
||||
|
@ -535,7 +514,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
|
||||
models.Delivery(inbox_url=remote_actor3.inbox_url),
|
||||
models.Delivery(inbox_url=remote_actor4.inbox_url),
|
||||
models.Delivery(inbox_url=library_follower_remote.inbox_url),
|
||||
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
|
||||
],
|
||||
key=lambda v: v.inbox_url,
|
||||
|
@ -549,7 +527,6 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
|||
remote_actor2.fid,
|
||||
remote_actor3.fid,
|
||||
activity.PUBLIC_ADDRESS,
|
||||
library.followers_url,
|
||||
followed_actor.followers_url,
|
||||
remote_actor4.fid,
|
||||
]
|
||||
|
|
|
@ -12,7 +12,6 @@ def test_library_serializer(factories, to_api_date):
|
|||
"uuid": str(library.uuid),
|
||||
"actor": serializers.APIActorSerializer(library.actor).data,
|
||||
"name": library.name,
|
||||
"description": library.description,
|
||||
"creation_date": to_api_date(library.creation_date),
|
||||
"uploads_count": library.uploads_count,
|
||||
"privacy_level": library.privacy_level,
|
||||
|
|
|
@ -29,7 +29,7 @@ def test_fix_fids_no_dry_run(factories, mocker, queryset_equal_queries):
|
|||
(music_models.Album, ["fid"]),
|
||||
(music_models.Track, ["fid"]),
|
||||
(music_models.Upload, ["fid"]),
|
||||
(music_models.Library, ["fid", "followers_url"]),
|
||||
(music_models.Library, ["fid"]),
|
||||
(
|
||||
federation_models.Actor,
|
||||
[
|
||||
|
|
|
@ -370,7 +370,7 @@ def test_outbox_create_audio(factories, mocker):
|
|||
}
|
||||
)
|
||||
expected = serializer.data
|
||||
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||
expected["to"] = [{"type": "followers", "target": upload.library.actor}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == upload.library.actor
|
||||
|
@ -685,7 +685,7 @@ def test_outbox_delete_audio(factories):
|
|||
{"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
|
||||
).data
|
||||
|
||||
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||
expected["to"] = [{"type": "followers", "target": upload.library.actor}]
|
||||
|
||||
assert dict(activity["payload"]) == dict(expected)
|
||||
assert activity["actor"] == upload.library.actor
|
||||
|
|
|
@ -548,13 +548,11 @@ def test_music_library_serializer_to_ap(factories):
|
|||
"type": "Library",
|
||||
"id": library.fid,
|
||||
"name": library.name,
|
||||
"summary": library.description,
|
||||
"attributedTo": library.actor.fid,
|
||||
"totalItems": 0,
|
||||
"current": library.fid + "?page=1",
|
||||
"last": library.fid + "?page=1",
|
||||
"first": library.fid + "?page=1",
|
||||
"followers": library.followers_url,
|
||||
}
|
||||
|
||||
assert serializer.data == expected
|
||||
|
@ -569,10 +567,8 @@ def test_music_library_serializer_from_public(factories, mocker):
|
|||
"@context": jsonld.get_default_context(),
|
||||
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"name": "Hello",
|
||||
"summary": "World",
|
||||
"type": "Library",
|
||||
"id": "https://library.id",
|
||||
"followers": "https://library.id/followers",
|
||||
"attributedTo": actor.fid,
|
||||
"totalItems": 12,
|
||||
"first": "https://library.id?page=1",
|
||||
|
@ -589,8 +585,6 @@ def test_music_library_serializer_from_public(factories, mocker):
|
|||
assert library.uploads_count == data["totalItems"]
|
||||
assert library.privacy_level == "everyone"
|
||||
assert library.name == "Hello"
|
||||
assert library.description == "World"
|
||||
assert library.followers_url == data["followers"]
|
||||
|
||||
retrieve.assert_called_once_with(
|
||||
actor.fid,
|
||||
|
@ -609,10 +603,8 @@ def test_music_library_serializer_from_private(factories, mocker):
|
|||
"@context": jsonld.get_default_context(),
|
||||
"audience": "",
|
||||
"name": "Hello",
|
||||
"summary": "World",
|
||||
"type": "Library",
|
||||
"id": "https://library.id",
|
||||
"followers": "https://library.id/followers",
|
||||
"attributedTo": actor.fid,
|
||||
"totalItems": 12,
|
||||
"first": "https://library.id?page=1",
|
||||
|
@ -629,8 +621,6 @@ def test_music_library_serializer_from_private(factories, mocker):
|
|||
assert library.uploads_count == data["totalItems"]
|
||||
assert library.privacy_level == "me"
|
||||
assert library.name == "Hello"
|
||||
assert library.description == "World"
|
||||
assert library.followers_url == data["followers"]
|
||||
retrieve.assert_called_once_with(
|
||||
actor.fid,
|
||||
actor=None,
|
||||
|
@ -647,10 +637,8 @@ def test_music_library_serializer_from_ap_update(factories, mocker):
|
|||
"@context": jsonld.get_default_context(),
|
||||
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
||||
"name": "Hello",
|
||||
"summary": "World",
|
||||
"type": "Library",
|
||||
"id": library.fid,
|
||||
"followers": "https://library.id/followers",
|
||||
"attributedTo": actor.fid,
|
||||
"totalItems": 12,
|
||||
"first": "https://library.id?page=1",
|
||||
|
@ -666,8 +654,6 @@ def test_music_library_serializer_from_ap_update(factories, mocker):
|
|||
assert library.uploads_count == data["totalItems"]
|
||||
assert library.privacy_level == "everyone"
|
||||
assert library.name == "Hello"
|
||||
assert library.description == "World"
|
||||
assert library.followers_url == data["followers"]
|
||||
|
||||
|
||||
def test_activity_pub_artist_serializer_to_ap(factories):
|
||||
|
|
|
@ -219,7 +219,6 @@ def test_music_library_retrieve_page_public(factories, api_client):
|
|||
"actor": library.actor,
|
||||
"page": Paginator([upload], 1).page(1),
|
||||
"name": library.name,
|
||||
"summary": library.description,
|
||||
}
|
||||
).data
|
||||
|
||||
|
|
|
@ -447,16 +447,13 @@ def test_manage_library_serializer(factories, now, to_api_date):
|
|||
"fid": library.fid,
|
||||
"url": library.url,
|
||||
"uuid": str(library.uuid),
|
||||
"followers_url": library.followers_url,
|
||||
"domain": library.domain_name,
|
||||
"is_local": library.is_local,
|
||||
"name": library.name,
|
||||
"description": library.description,
|
||||
"privacy_level": library.privacy_level,
|
||||
"creation_date": to_api_date(library.creation_date),
|
||||
"actor": serializers.ManageBaseActorSerializer(library.actor).data,
|
||||
"uploads_count": 44,
|
||||
"followers_count": 42,
|
||||
}
|
||||
s = serializers.ManageLibrarySerializer(library)
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from django.utils.timezone import now
|
||||
|
||||
# this test is commented since it's very slow, but it can be useful for future development
|
||||
# def test_pytest_plugin_initial(migrator):
|
||||
# mapping_list = [
|
||||
|
@ -72,3 +77,107 @@ def test_artist_credit_migration(migrator):
|
|||
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
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_migrate_libraries_to_playlist(migrator):
|
||||
music_initial_migration = (
|
||||
"music",
|
||||
"0059_remove_album_artist_remove_track_artist_artistcredit_and_more",
|
||||
)
|
||||
music_final_migration = ("music", "0061_migrate_libraries_to_playlist")
|
||||
|
||||
# Apply migrations
|
||||
migrator.migrate(
|
||||
[
|
||||
music_initial_migration,
|
||||
]
|
||||
)
|
||||
music_apps = migrator.loader.project_state([music_initial_migration]).apps
|
||||
|
||||
Playlist = music_apps.get_model("playlists", "Playlist")
|
||||
LibraryFollow = music_apps.get_model("federation", "LibraryFollow")
|
||||
Actor = music_apps.get_model("federation", "Actor")
|
||||
Domain = music_apps.get_model("federation", "Domain")
|
||||
Track = music_apps.get_model("music", "Track")
|
||||
Library = music_apps.get_model("music", "Library")
|
||||
Upload = music_apps.get_model("music", "Upload")
|
||||
|
||||
# Create data
|
||||
domain = Domain.objects.create()
|
||||
domain2 = Domain.objects.create(pk=2)
|
||||
actor = Actor.objects.create(name="Test Actor", domain=domain)
|
||||
existing_urls = Actor.objects.values_list("fid", flat=True)
|
||||
print(existing_urls)
|
||||
target_actor = Actor.objects.create(
|
||||
name="Test Actor 2", domain=domain2, fid="http://test2.com/superduniquemanonmam"
|
||||
)
|
||||
|
||||
library = Library.objects.create(
|
||||
name="This should becane playlist name",
|
||||
actor=target_actor,
|
||||
creation_date=now(),
|
||||
privacy_level="everyone",
|
||||
uuid=uuid4(),
|
||||
)
|
||||
|
||||
Track.objects.create()
|
||||
Track.objects.create()
|
||||
track = Track.objects.create()
|
||||
track2 = Track.objects.create()
|
||||
track3 = Track.objects.create()
|
||||
|
||||
uploads = [
|
||||
Upload.objects.create(library=library, track=track),
|
||||
Upload.objects.create(library=library, track=track2),
|
||||
Upload.objects.create(library=library, track=track3),
|
||||
]
|
||||
|
||||
library_follow = LibraryFollow.objects.create(
|
||||
uuid=uuid4(),
|
||||
target=library,
|
||||
actor=actor,
|
||||
approved=True,
|
||||
creation_date=now(),
|
||||
modification_date=now(),
|
||||
)
|
||||
|
||||
# Perform migration
|
||||
migrator.loader.build_graph()
|
||||
migrator.migrate([music_final_migration])
|
||||
|
||||
new_apps = migrator.loader.project_state([music_final_migration]).apps
|
||||
Playlist = new_apps.get_model("playlists", "Playlist")
|
||||
PlaylistTrack = new_apps.get_model("playlists", "PlaylistTrack")
|
||||
Follow = new_apps.get_model("federation", "Follow")
|
||||
LibraryFollow = new_apps.get_model("federation", "LibraryFollow")
|
||||
Follow = new_apps.get_model("federation", "Follow")
|
||||
|
||||
# Assertions
|
||||
|
||||
# Verify Playlist creation
|
||||
playlist = Playlist.objects.get(name="This should becane playlist name")
|
||||
|
||||
assert playlist.actor.pk == library.actor.pk
|
||||
assert playlist.creation_date == library.creation_date
|
||||
assert playlist.privacy_level == library.privacy_level
|
||||
assert playlist.description == library.description
|
||||
|
||||
# Verify PlaylistTrack creation
|
||||
playlist_tracks = PlaylistTrack.objects.filter(playlist=playlist).order_by("index")
|
||||
assert playlist_tracks.count() == 3
|
||||
for i, playlist_track in enumerate(playlist_tracks):
|
||||
assert playlist_track.track.pk == uploads[i].track.pk
|
||||
|
||||
# Verify User Follow creation
|
||||
follow = Follow.objects.get(target__pk=target_actor.pk)
|
||||
assert follow.actor.pk == actor.pk
|
||||
assert follow.approved == library_follow.approved
|
||||
|
||||
# Verify LibraryFollow deletion and library creation
|
||||
assert LibraryFollow.objects.count() == 0
|
||||
|
||||
# Test fail but works on real db I don't get why
|
||||
# no library are found in the new app
|
||||
# NewAppLibrary = new_apps.get_model("music", "Library")
|
||||
# assert NewAppLibrary.objects.count() == 3
|
||||
|
|
|
@ -263,7 +263,7 @@ def test_library(factories):
|
|||
now = timezone.now()
|
||||
actor = factories["federation.Actor"]()
|
||||
library = factories["music.Library"](
|
||||
name="Hello world", description="hello", actor=actor, privacy_level="instance"
|
||||
name="Hello world", actor=actor, privacy_level="instance"
|
||||
)
|
||||
|
||||
assert library.creation_date >= now
|
||||
|
|
|
@ -406,31 +406,6 @@ def test_track_upload_serializer(factories):
|
|||
assert data == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"field,before,after",
|
||||
[
|
||||
("privacy_level", "me", "everyone"),
|
||||
("name", "Before", "After"),
|
||||
("description", "Before", "After"),
|
||||
],
|
||||
)
|
||||
def test_update_library_privacy_level_broadcasts_to_followers(
|
||||
factories, field, before, after, mocker
|
||||
):
|
||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
library = factories["music.Library"](**{field: before})
|
||||
|
||||
serializer = serializers.LibraryForOwnerSerializer(
|
||||
library, data={field: after}, partial=True
|
||||
)
|
||||
assert serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
|
||||
dispatch.assert_called_once_with(
|
||||
{"type": "Update", "object": {"type": "Library"}}, context={"library": library}
|
||||
)
|
||||
|
||||
|
||||
def test_upload_with_channel(factories, uploaded_audio_file):
|
||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||
user = channel.attributed_to.user
|
||||
|
|
|
@ -311,7 +311,6 @@ def test_library_library(spa_html, no_api_auth, client, factories, settings):
|
|||
},
|
||||
{"tag": "meta", "property": "og:type", "content": "website"},
|
||||
{"tag": "meta", "property": "og:title", "content": library.name},
|
||||
{"tag": "meta", "property": "og:description", "content": library.description},
|
||||
{
|
||||
"tag": "link",
|
||||
"rel": "alternate",
|
||||
|
|
|
@ -640,25 +640,6 @@ def test_listen_transcode_in_place(
|
|||
)
|
||||
|
||||
|
||||
def test_user_can_create_library(factories, logged_in_api_client):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
url = reverse("api:v1:libraries-list")
|
||||
|
||||
response = logged_in_api_client.post(
|
||||
url, {"name": "hello", "description": "world", "privacy_level": "me"}
|
||||
)
|
||||
library = actor.libraries.first()
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
assert library.actor == actor
|
||||
assert library.name == "hello"
|
||||
assert library.description == "world"
|
||||
assert library.privacy_level == "me"
|
||||
assert library.fid == library.get_federation_id()
|
||||
assert library.followers_url == library.fid + "/followers"
|
||||
|
||||
|
||||
def test_user_can_list_their_library(factories, logged_in_api_client):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
library = factories["music.Library"](actor=actor)
|
||||
|
@ -712,17 +693,7 @@ def test_user_cannot_delete_other_actors_library(factories, logged_in_api_client
|
|||
url = reverse("api:v1:libraries-detail", kwargs={"uuid": library.uuid})
|
||||
response = logged_in_api_client.delete(url)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_library_delete_via_api_triggers_outbox(factories, mocker):
|
||||
dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
library = factories["music.Library"]()
|
||||
view = views.LibraryViewSet()
|
||||
view.perform_destroy(library)
|
||||
dispatch.assert_called_once_with(
|
||||
{"type": "Delete", "object": {"type": "Library"}}, context={"library": library}
|
||||
)
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
def test_user_cannot_get_other_not_playable_uploads(factories, logged_in_api_client):
|
||||
|
@ -937,25 +908,6 @@ def test_user_can_patch_draft_upload_status_triggers_processing(
|
|||
m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk)
|
||||
|
||||
|
||||
def test_user_can_list_own_library_follows(factories, logged_in_api_client):
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
library = factories["music.Library"](actor=actor)
|
||||
another_library = factories["music.Library"](actor=actor)
|
||||
follow = factories["federation.LibraryFollow"](target=library)
|
||||
factories["federation.LibraryFollow"](target=another_library)
|
||||
|
||||
url = reverse("api:v1:libraries-follows", kwargs={"uuid": library.uuid})
|
||||
|
||||
response = logged_in_api_client.get(url)
|
||||
|
||||
assert response.data == {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [federation_api_serializers.LibraryFollowSerializer(follow).data],
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("entity", ["artist", "album", "track"])
|
||||
def test_can_get_libraries_for_music_entities(
|
||||
factories, api_client, entity, preferences
|
||||
|
@ -1615,3 +1567,25 @@ def test_album_create_artist_credit(factories, logged_in_api_client):
|
|||
url, {"artist": artist.pk, "title": "super album"}, format="json"
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_can_patch_upload_list(factories, logged_in_api_client):
|
||||
url = reverse("api:v1:uploads-bulk-update")
|
||||
actor = logged_in_api_client.user.create_actor()
|
||||
upload = factories["music.Upload"](library__actor=actor)
|
||||
upload2 = factories["music.Upload"](library__actor=actor)
|
||||
factories["music.Library"](actor=actor, privacy_level="everyone")
|
||||
|
||||
response = logged_in_api_client.patch(
|
||||
url,
|
||||
[
|
||||
{"uuid": upload.uuid, "privacy_level": "everyone"},
|
||||
{"uuid": upload2.uuid, "privacy_level": "everyone"},
|
||||
],
|
||||
format="json",
|
||||
)
|
||||
upload.refresh_from_db()
|
||||
upload2.refresh_from_db()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert upload.library.privacy_level == "everyone"
|
||||
|
|
|
@ -89,3 +89,30 @@ def test_playlist_serializer(factories, to_api_date):
|
|||
serializer = serializers.PlaylistSerializer(playlist)
|
||||
|
||||
assert serializer.data == expected
|
||||
|
||||
|
||||
# to do :
|
||||
|
||||
# @pytest.mark.parametrize(
|
||||
# "field,before,after",
|
||||
# [
|
||||
# ("privacy_level", "me", "everyone"),
|
||||
# ("name", "Before", "After"),
|
||||
# ("description", "Before", "After"),
|
||||
# ],
|
||||
# )
|
||||
# def test_update_playlist_privacy_level_broadcasts_to_followers(
|
||||
# factories, field, before, after, mocker
|
||||
# ):
|
||||
# dispatch = mocker.patch("funkwhale_api.federation.routes.outbox.dispatch")
|
||||
# playlist = factories["playlists.Playlist"](**{field: before})
|
||||
|
||||
# serializer = serializers.PlaylistSerializer(
|
||||
# playlist, data={field: after}, partial=True
|
||||
# )
|
||||
# assert serializer.is_valid(raise_exception=True)
|
||||
# serializer.save()
|
||||
|
||||
# dispatch.assert_called_once_with(
|
||||
# {"type": "Update", "object": {"type": "Library"}}, context={"library": library}
|
||||
# )
|
||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
|||
from django.urls import reverse
|
||||
|
||||
from funkwhale_api.federation import utils as federation_utils
|
||||
from funkwhale_api.music import models as music_models
|
||||
from funkwhale_api.users import models
|
||||
|
||||
|
||||
|
@ -182,6 +183,18 @@ def test_creating_actor_from_user(factories, settings):
|
|||
)
|
||||
|
||||
|
||||
def test_creating_libraries_from_user(factories, settings):
|
||||
user = factories["users.User"](username="Hello M. world", with_actor=True)
|
||||
models.create_user_libraries(user)
|
||||
for privacy_level, desc in music_models.LIBRARY_PRIVACY_LEVEL_CHOICES:
|
||||
assert (
|
||||
user.actor.libraries.filter(
|
||||
name=privacy_level, privacy_level=privacy_level, actor=user.actor
|
||||
).count()
|
||||
== 1
|
||||
)
|
||||
|
||||
|
||||
def test_get_channels_groups(factories):
|
||||
user = factories["users.User"](permission_library=True)
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Drop libraries in favor of playlist (#2366)
|
|
@ -23,7 +23,6 @@ services:
|
|||
dockerfile: Dockerfile.debian
|
||||
command: >
|
||||
sh -c "
|
||||
funkwhale-manage migrate &&
|
||||
funkwhale-manage collectstatic --no-input &&
|
||||
uvicorn --reload config.asgi:application --host 0.0.0.0 --port 5000 --reload-dir config/ --reload-dir funkwhale_api/
|
||||
"
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# Library drop in favor of playlist
|
||||
|
||||
## Issue
|
||||
|
||||
We now have playlist, use complained about library not being clearly defined.
|
||||
|
||||
## Proposed solution
|
||||
|
||||
## Backend
|
||||
|
||||
A new endpoint to move upload from one library to another : `PATCH` on `api/v2/uploads/bulk-update` with `[{"uuid": "uuid1", "privacy_level": "public"}]`
|
||||
|
||||
### Migration from Libraries to Playlist
|
||||
|
||||
New `description` field on playlist, to inherit from the `description` field of Library
|
||||
|
||||
Library Follows will be transformed to user follow.
|
||||
The schedule_scan function of the library still exist and allow federation of audio content
|
||||
|
||||
During user creation, built-in libraries are generated automatically by `create_user_libraries`
|
Loading…
Reference in New Issue