Revert "Drop libraries in favor of playlist for user audio sharing (#2366)"
This reverts commit c9d915fb33
.
This commit is contained in:
parent
deda2f2b08
commit
be90a29d76
|
@ -49,7 +49,6 @@ def handler_create_user(
|
||||||
utils.logger.warn("Unknown permission %s", permission)
|
utils.logger.warn("Unknown permission %s", permission)
|
||||||
utils.logger.debug("Creating actor…")
|
utils.logger.debug("Creating actor…")
|
||||||
user.actor = models.create_actor(user)
|
user.actor = models.create_actor(user)
|
||||||
models.create_user_libraries(user)
|
|
||||||
user.save()
|
user.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
|
@ -627,6 +627,12 @@ def get_actors_from_audience(urls):
|
||||||
final_query, Q(pk__in=actor_follows.values_list("actor", flat=True))
|
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:
|
if not final_query:
|
||||||
return models.Actor.objects.none()
|
return models.Actor.objects.none()
|
||||||
return models.Actor.objects.filter(final_query)
|
return models.Actor.objects.filter(final_query)
|
||||||
|
|
|
@ -56,6 +56,7 @@ class LibrarySerializer(serializers.ModelSerializer):
|
||||||
"uuid",
|
"uuid",
|
||||||
"actor",
|
"actor",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"uploads_count",
|
"uploads_count",
|
||||||
"privacy_level",
|
"privacy_level",
|
||||||
|
|
|
@ -188,9 +188,13 @@ class MusicLibraryFactory(NoUpdateOnCreate, factory.django.DjangoModelFactory):
|
||||||
uuid = factory.Faker("uuid4")
|
uuid = factory.Faker("uuid4")
|
||||||
actor = factory.SubFactory(ActorFactory)
|
actor = factory.SubFactory(ActorFactory)
|
||||||
privacy_level = "me"
|
privacy_level = "me"
|
||||||
name = privacy_level
|
name = factory.Faker("sentence")
|
||||||
|
description = factory.Faker("sentence")
|
||||||
uploads_count = 0
|
uploads_count = 0
|
||||||
fid = factory.Faker("federation_url")
|
fid = factory.Faker("federation_url")
|
||||||
|
followers_url = factory.LazyAttribute(
|
||||||
|
lambda o: o.fid + "/followers" if o.fid else None
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = "music.Library"
|
model = "music.Library"
|
||||||
|
|
|
@ -9,7 +9,7 @@ MODELS = [
|
||||||
(music_models.Album, ["fid"]),
|
(music_models.Album, ["fid"]),
|
||||||
(music_models.Track, ["fid"]),
|
(music_models.Track, ["fid"]),
|
||||||
(music_models.Upload, ["fid"]),
|
(music_models.Upload, ["fid"]),
|
||||||
(music_models.Library, ["fid"]),
|
(music_models.Library, ["fid", "followers_url"]),
|
||||||
(
|
(
|
||||||
federation_models.Actor,
|
federation_models.Actor,
|
||||||
[
|
[
|
||||||
|
|
|
@ -166,7 +166,7 @@ def outbox_follow(context):
|
||||||
def outbox_create_audio(context):
|
def outbox_create_audio(context):
|
||||||
upload = context["upload"]
|
upload = context["upload"]
|
||||||
channel = upload.library.get_channel()
|
channel = upload.library.get_channel()
|
||||||
followers_target = channel.actor if channel else upload.library.actor
|
followers_target = channel.actor if channel else upload.library
|
||||||
actor = channel.actor if channel else upload.library.actor
|
actor = channel.actor if channel else upload.library.actor
|
||||||
if channel:
|
if channel:
|
||||||
serializer = serializers.ChannelCreateUploadSerializer(upload)
|
serializer = serializers.ChannelCreateUploadSerializer(upload)
|
||||||
|
@ -310,8 +310,8 @@ def outbox_delete_audio(context):
|
||||||
uploads = context["uploads"]
|
uploads = context["uploads"]
|
||||||
library = uploads[0].library
|
library = uploads[0].library
|
||||||
channel = library.get_channel()
|
channel = library.get_channel()
|
||||||
|
followers_target = channel.actor if channel else library
|
||||||
actor = channel.actor if channel else library.actor
|
actor = channel.actor if channel else library.actor
|
||||||
followers_target = channel.actor if channel else actor
|
|
||||||
serializer = serializers.ActivitySerializer(
|
serializer = serializers.ActivitySerializer(
|
||||||
{
|
{
|
||||||
"type": "Delete",
|
"type": "Delete",
|
||||||
|
@ -679,9 +679,6 @@ def inbox_delete_favorite(payload, context):
|
||||||
favorite.delete()
|
favorite.delete()
|
||||||
|
|
||||||
|
|
||||||
# to do : test listening routes and broadcast
|
|
||||||
|
|
||||||
|
|
||||||
@outbox.register({"type": "Listen", "object.type": "Track"})
|
@outbox.register({"type": "Listen", "object.type": "Track"})
|
||||||
def outbox_create_listening(context):
|
def outbox_create_listening(context):
|
||||||
track = context["track"]
|
track = context["track"]
|
||||||
|
|
|
@ -996,6 +996,8 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
||||||
actor = serializers.URLField(max_length=500, required=False)
|
actor = serializers.URLField(max_length=500, required=False)
|
||||||
attributedTo = serializers.URLField(max_length=500, required=False)
|
attributedTo = serializers.URLField(max_length=500, required=False)
|
||||||
name = serializers.CharField()
|
name = serializers.CharField()
|
||||||
|
summary = serializers.CharField(allow_blank=True, allow_null=True, required=False)
|
||||||
|
followers = serializers.URLField(max_length=500)
|
||||||
audience = serializers.ChoiceField(
|
audience = serializers.ChoiceField(
|
||||||
choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
|
choices=["", "./", None, "https://www.w3.org/ns/activitystreams#Public"],
|
||||||
required=False,
|
required=False,
|
||||||
|
@ -1012,7 +1014,9 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
||||||
PAGINATED_COLLECTION_JSONLD_MAPPING,
|
PAGINATED_COLLECTION_JSONLD_MAPPING,
|
||||||
{
|
{
|
||||||
"name": jsonld.first_val(contexts.AS.name),
|
"name": jsonld.first_val(contexts.AS.name),
|
||||||
|
"summary": jsonld.first_val(contexts.AS.summary),
|
||||||
"audience": jsonld.first_id(contexts.AS.audience),
|
"audience": jsonld.first_id(contexts.AS.audience),
|
||||||
|
"followers": jsonld.first_id(contexts.AS.followers),
|
||||||
"actor": jsonld.first_id(contexts.AS.actor),
|
"actor": jsonld.first_id(contexts.AS.actor),
|
||||||
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
|
"attributedTo": jsonld.first_id(contexts.AS.attributedTo),
|
||||||
},
|
},
|
||||||
|
@ -1034,6 +1038,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
||||||
conf = {
|
conf = {
|
||||||
"id": library.fid,
|
"id": library.fid,
|
||||||
"name": library.name,
|
"name": library.name,
|
||||||
|
"summary": library.description,
|
||||||
"page_size": 100,
|
"page_size": 100,
|
||||||
"attributedTo": library.actor,
|
"attributedTo": library.actor,
|
||||||
"actor": library.actor,
|
"actor": library.actor,
|
||||||
|
@ -1044,6 +1049,7 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
||||||
r["audience"] = (
|
r["audience"] = (
|
||||||
contexts.AS.Public if library.privacy_level == "everyone" else ""
|
contexts.AS.Public if library.privacy_level == "everyone" else ""
|
||||||
)
|
)
|
||||||
|
r["followers"] = library.followers_url
|
||||||
return r
|
return r
|
||||||
|
|
||||||
def create(self, validated_data):
|
def create(self, validated_data):
|
||||||
|
@ -1063,6 +1069,8 @@ class LibrarySerializer(PaginatedCollectionSerializer):
|
||||||
defaults={
|
defaults={
|
||||||
"uploads_count": validated_data["totalItems"],
|
"uploads_count": validated_data["totalItems"],
|
||||||
"name": validated_data["name"],
|
"name": validated_data["name"],
|
||||||
|
"description": validated_data.get("summary"),
|
||||||
|
"followers_url": validated_data["followers"],
|
||||||
"privacy_level": privacy[validated_data["audience"]],
|
"privacy_level": privacy[validated_data["audience"]],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -387,6 +387,7 @@ class MusicLibraryViewSet(
|
||||||
"id": lb.get_federation_id(),
|
"id": lb.get_federation_id(),
|
||||||
"actor": lb.actor,
|
"actor": lb.actor,
|
||||||
"name": lb.name,
|
"name": lb.name,
|
||||||
|
"summary": lb.description,
|
||||||
"items": lb.uploads.for_federation()
|
"items": lb.uploads.for_federation()
|
||||||
.order_by("-creation_date")
|
.order_by("-creation_date")
|
||||||
.prefetch_related(
|
.prefetch_related(
|
||||||
|
|
|
@ -572,6 +572,7 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
||||||
domain = serializers.CharField(source="domain_name")
|
domain = serializers.CharField(source="domain_name")
|
||||||
actor = ManageBaseActorSerializer()
|
actor = ManageBaseActorSerializer()
|
||||||
uploads_count = serializers.SerializerMethodField()
|
uploads_count = serializers.SerializerMethodField()
|
||||||
|
followers_count = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = music_models.Library
|
model = music_models.Library
|
||||||
|
@ -581,11 +582,14 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
||||||
"fid",
|
"fid",
|
||||||
"url",
|
"url",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"domain",
|
"domain",
|
||||||
"is_local",
|
"is_local",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"privacy_level",
|
"privacy_level",
|
||||||
"uploads_count",
|
"uploads_count",
|
||||||
|
"followers_count",
|
||||||
|
"followers_url",
|
||||||
"actor",
|
"actor",
|
||||||
]
|
]
|
||||||
read_only_fields = [
|
read_only_fields = [
|
||||||
|
@ -601,6 +605,10 @@ class ManageLibrarySerializer(serializers.ModelSerializer):
|
||||||
def get_uploads_count(self, obj) -> int:
|
def get_uploads_count(self, obj) -> int:
|
||||||
return getattr(obj, "_uploads_count", int(obj.uploads_count))
|
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):
|
class ManageNestedLibrarySerializer(serializers.ModelSerializer):
|
||||||
domain = serializers.CharField(source="domain_name")
|
domain = serializers.CharField(source="domain_name")
|
||||||
|
@ -614,10 +622,12 @@ class ManageNestedLibrarySerializer(serializers.ModelSerializer):
|
||||||
"fid",
|
"fid",
|
||||||
"url",
|
"url",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"domain",
|
"domain",
|
||||||
"is_local",
|
"is_local",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"privacy_level",
|
"privacy_level",
|
||||||
|
"followers_url",
|
||||||
"actor",
|
"actor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
@ -164,6 +164,7 @@ class LibraryStateSerializer(serializers.ModelSerializer):
|
||||||
"uuid",
|
"uuid",
|
||||||
"fid",
|
"fid",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"creation_date",
|
"creation_date",
|
||||||
"privacy_level",
|
"privacy_level",
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,7 +12,7 @@ from funkwhale_api.federation import factories as federation_factories
|
||||||
from funkwhale_api.history import factories as history_factories
|
from funkwhale_api.history import factories as history_factories
|
||||||
from funkwhale_api.music import factories as music_factories
|
from funkwhale_api.music import factories as music_factories
|
||||||
from funkwhale_api.playlists import factories as playlist_factories
|
from funkwhale_api.playlists import factories as playlist_factories
|
||||||
from funkwhale_api.users import models, serializers
|
from funkwhale_api.users import serializers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -39,7 +39,6 @@ def create_data(super_user_name=None):
|
||||||
print(
|
print(
|
||||||
f"Superuser {super_user_name} already in db. Skipping superuser creation"
|
f"Superuser {super_user_name} already in db. Skipping superuser creation"
|
||||||
)
|
)
|
||||||
super_user = models.User.objects.get(username=super_user_name)
|
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
|
@ -36,7 +36,6 @@ def set_all_artists_credit(apps, schema_editor):
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("music", "0058_upload_quality"),
|
("music", "0058_upload_quality"),
|
||||||
("playlists", "0008_playlist_library_drop"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
# 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",
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1,108 +0,0 @@
|
||||||
# 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
|
|
||||||
),
|
|
||||||
]
|
|
|
@ -1335,8 +1335,10 @@ class Library(federation_models.FederationMixin):
|
||||||
actor = models.ForeignKey(
|
actor = models.ForeignKey(
|
||||||
"federation.Actor", related_name="libraries", on_delete=models.CASCADE
|
"federation.Actor", related_name="libraries", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
followers_url = models.URLField(max_length=500)
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
description = models.TextField(max_length=5000, null=True, blank=True)
|
||||||
privacy_level = models.CharField(
|
privacy_level = models.CharField(
|
||||||
choices=LIBRARY_PRIVACY_LEVEL_CHOICES, default="me", max_length=25
|
choices=LIBRARY_PRIVACY_LEVEL_CHOICES, default="me", max_length=25
|
||||||
)
|
)
|
||||||
|
|
|
@ -338,6 +338,7 @@ class LibraryForOwnerSerializer(serializers.ModelSerializer):
|
||||||
"uuid",
|
"uuid",
|
||||||
"fid",
|
"fid",
|
||||||
"name",
|
"name",
|
||||||
|
"description",
|
||||||
"privacy_level",
|
"privacy_level",
|
||||||
"uploads_count",
|
"uploads_count",
|
||||||
"size",
|
"size",
|
||||||
|
@ -536,26 +537,6 @@ class UploadForOwnerSerializer(UploadSerializer):
|
||||||
return f
|
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):
|
class UploadActionSerializer(common_serializers.ActionSerializer):
|
||||||
actions = [
|
actions = [
|
||||||
common_serializers.Action("delete", allow_all=True),
|
common_serializers.Action("delete", allow_all=True),
|
||||||
|
|
|
@ -340,6 +340,7 @@ def library_library(request, uuid, redirect_to_ap):
|
||||||
{"tag": "meta", "property": "og:url", "content": library_url},
|
{"tag": "meta", "property": "og:url", "content": library_url},
|
||||||
{"tag": "meta", "property": "og:type", "content": "website"},
|
{"tag": "meta", "property": "og:type", "content": "website"},
|
||||||
{"tag": "meta", "property": "og:title", "content": obj.name},
|
{"tag": "meta", "property": "og:title", "content": obj.name},
|
||||||
|
{"tag": "meta", "property": "og:description", "content": obj.description},
|
||||||
]
|
]
|
||||||
|
|
||||||
if preferences.get("federation__enabled"):
|
if preferences.get("federation__enabled"):
|
||||||
|
|
|
@ -293,8 +293,11 @@ class AlbumViewSet(
|
||||||
|
|
||||||
|
|
||||||
class LibraryViewSet(
|
class LibraryViewSet(
|
||||||
|
mixins.CreateModelMixin,
|
||||||
mixins.ListModelMixin,
|
mixins.ListModelMixin,
|
||||||
mixins.RetrieveModelMixin,
|
mixins.RetrieveModelMixin,
|
||||||
|
mixins.UpdateModelMixin,
|
||||||
|
mixins.DestroyModelMixin,
|
||||||
viewsets.GenericViewSet,
|
viewsets.GenericViewSet,
|
||||||
):
|
):
|
||||||
lookup_field = "uuid"
|
lookup_field = "uuid"
|
||||||
|
@ -329,6 +332,42 @@ class LibraryViewSet(
|
||||||
|
|
||||||
return qs
|
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
|
# TODO quickfix, basically specifying the response would be None
|
||||||
@extend_schema(responses=None)
|
@extend_schema(responses=None)
|
||||||
@action(
|
@action(
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# 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):
|
class Playlist(federation_models.FederationMixin):
|
||||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=50)
|
||||||
actor = models.ForeignKey(
|
actor = models.ForeignKey(
|
||||||
"federation.Actor", related_name="playlists", on_delete=models.CASCADE
|
"federation.Actor", related_name="playlists", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
creation_date = models.DateTimeField(default=timezone.now)
|
creation_date = models.DateTimeField(default=timezone.now)
|
||||||
modification_date = models.DateTimeField(auto_now=True)
|
modification_date = models.DateTimeField(auto_now=True)
|
||||||
privacy_level = fields.get_privacy_field()
|
privacy_level = fields.get_privacy_field()
|
||||||
description = models.TextField(max_length=5000, null=True, blank=True)
|
|
||||||
objects = PlaylistQuerySet.as_manager()
|
objects = PlaylistQuerySet.as_manager()
|
||||||
federation_namespace = "playlists"
|
federation_namespace = "playlists"
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,6 @@ from funkwhale_api.common import validators as common_validators
|
||||||
from funkwhale_api.federation import keys
|
from funkwhale_api.federation import keys
|
||||||
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
|
||||||
from funkwhale_api.music import models as music_models
|
|
||||||
|
|
||||||
|
|
||||||
def get_token(length=5):
|
def get_token(length=5):
|
||||||
|
@ -462,22 +461,6 @@ def create_actor(user, **kwargs):
|
||||||
return actor
|
return actor
|
||||||
|
|
||||||
|
|
||||||
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)
|
@receiver(ldap_populate_user)
|
||||||
def init_ldap_user(sender, user, ldap_user, **kwargs):
|
def init_ldap_user(sender, user, ldap_user, **kwargs):
|
||||||
if not user.actor:
|
if not user.actor:
|
||||||
|
|
|
@ -114,7 +114,7 @@ class RegisterSerializer(RS):
|
||||||
user_request_id=user_request.pk,
|
user_request_id=user_request.pk,
|
||||||
new_status=user_request.status,
|
new_status=user_request.status,
|
||||||
)
|
)
|
||||||
models.create_user_libraries(user)
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
@ -244,6 +246,9 @@ def test_should_reject(factories, params, policy_kwargs, expected):
|
||||||
|
|
||||||
def test_get_actors_from_audience_urls(settings, db):
|
def test_get_actors_from_audience_urls(settings, db):
|
||||||
settings.FEDERATION_HOSTNAME = "federation.hostname"
|
settings.FEDERATION_HOSTNAME = "federation.hostname"
|
||||||
|
library_uuid1 = uuid.uuid4()
|
||||||
|
library_uuid2 = uuid.uuid4()
|
||||||
|
|
||||||
urls = [
|
urls = [
|
||||||
"https://wrong.url",
|
"https://wrong.url",
|
||||||
"https://federation.hostname"
|
"https://federation.hostname"
|
||||||
|
@ -252,15 +257,21 @@ def test_get_actors_from_audience_urls(settings, db):
|
||||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
|
+ reverse("federation:actors-detail", kwargs={"preferred_username": "alice"}),
|
||||||
"https://federation.hostname"
|
"https://federation.hostname"
|
||||||
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
|
+ reverse("federation:actors-detail", kwargs={"preferred_username": "bob"}),
|
||||||
"https://federation.hostname",
|
"https://federation.hostname"
|
||||||
|
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid1}),
|
||||||
|
"https://federation.hostname"
|
||||||
|
+ reverse("federation:music:libraries-detail", kwargs={"uuid": library_uuid2}),
|
||||||
activity.PUBLIC_ADDRESS,
|
activity.PUBLIC_ADDRESS,
|
||||||
]
|
]
|
||||||
followed_query = Q(target__followers_url=urls[0])
|
followed_query = Q(target__followers_url=urls[0])
|
||||||
for url in urls[1:-1]:
|
for url in urls[1:-1]:
|
||||||
followed_query |= Q(target__followers_url=url)
|
followed_query |= Q(target__followers_url=url)
|
||||||
actor_follows = models.Follow.objects.filter(followed_query, approved=True)
|
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(
|
expected = models.Actor.objects.filter(
|
||||||
Q(fid__in=urls[0:-1]) | Q(pk__in=actor_follows.values_list("actor", flat=True))
|
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))
|
||||||
)
|
)
|
||||||
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
|
assert str(activity.get_actors_from_audience(urls).query) == str(expected.query)
|
||||||
|
|
||||||
|
@ -467,9 +478,17 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
||||||
)
|
)
|
||||||
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
|
remote_actor3 = factories["federation.Actor"](shared_inbox_url=None)
|
||||||
remote_actor4 = factories["federation.Actor"]()
|
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
|
# follow not approved
|
||||||
factories["federation.Follow"](
|
factories["federation.LibraryFollow"](
|
||||||
target=remote_actor3, actor__local=False, approved=False
|
target=library, actor__local=False, approved=False
|
||||||
)
|
)
|
||||||
|
|
||||||
followed_actor = factories["federation.Actor"]()
|
followed_actor = factories["federation.Actor"]()
|
||||||
|
@ -492,6 +511,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
||||||
remote_actor2,
|
remote_actor2,
|
||||||
remote_actor3,
|
remote_actor3,
|
||||||
activity.PUBLIC_ADDRESS,
|
activity.PUBLIC_ADDRESS,
|
||||||
|
{"type": "followers", "target": library},
|
||||||
{"type": "followers", "target": followed_actor},
|
{"type": "followers", "target": followed_actor},
|
||||||
{"type": "actor_inbox", "actor": remote_actor4},
|
{"type": "actor_inbox", "actor": remote_actor4},
|
||||||
]
|
]
|
||||||
|
@ -504,6 +524,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
||||||
models.InboxItem(actor=local_actor1, type="to"),
|
models.InboxItem(actor=local_actor1, type="to"),
|
||||||
models.InboxItem(actor=local_actor2, type="to"),
|
models.InboxItem(actor=local_actor2, type="to"),
|
||||||
models.InboxItem(actor=local_actor3, 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"),
|
models.InboxItem(actor=actor_follower_local, type="to"),
|
||||||
],
|
],
|
||||||
key=lambda v: v.actor.pk,
|
key=lambda v: v.actor.pk,
|
||||||
|
@ -514,6 +535,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
||||||
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
|
models.Delivery(inbox_url=remote_actor1.shared_inbox_url),
|
||||||
models.Delivery(inbox_url=remote_actor3.inbox_url),
|
models.Delivery(inbox_url=remote_actor3.inbox_url),
|
||||||
models.Delivery(inbox_url=remote_actor4.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),
|
models.Delivery(inbox_url=actor_follower_remote.inbox_url),
|
||||||
],
|
],
|
||||||
key=lambda v: v.inbox_url,
|
key=lambda v: v.inbox_url,
|
||||||
|
@ -527,6 +549,7 @@ def test_prepare_deliveries_and_inbox_items(factories, preferences):
|
||||||
remote_actor2.fid,
|
remote_actor2.fid,
|
||||||
remote_actor3.fid,
|
remote_actor3.fid,
|
||||||
activity.PUBLIC_ADDRESS,
|
activity.PUBLIC_ADDRESS,
|
||||||
|
library.followers_url,
|
||||||
followed_actor.followers_url,
|
followed_actor.followers_url,
|
||||||
remote_actor4.fid,
|
remote_actor4.fid,
|
||||||
]
|
]
|
||||||
|
|
|
@ -12,6 +12,7 @@ def test_library_serializer(factories, to_api_date):
|
||||||
"uuid": str(library.uuid),
|
"uuid": str(library.uuid),
|
||||||
"actor": serializers.APIActorSerializer(library.actor).data,
|
"actor": serializers.APIActorSerializer(library.actor).data,
|
||||||
"name": library.name,
|
"name": library.name,
|
||||||
|
"description": library.description,
|
||||||
"creation_date": to_api_date(library.creation_date),
|
"creation_date": to_api_date(library.creation_date),
|
||||||
"uploads_count": library.uploads_count,
|
"uploads_count": library.uploads_count,
|
||||||
"privacy_level": library.privacy_level,
|
"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.Album, ["fid"]),
|
||||||
(music_models.Track, ["fid"]),
|
(music_models.Track, ["fid"]),
|
||||||
(music_models.Upload, ["fid"]),
|
(music_models.Upload, ["fid"]),
|
||||||
(music_models.Library, ["fid"]),
|
(music_models.Library, ["fid", "followers_url"]),
|
||||||
(
|
(
|
||||||
federation_models.Actor,
|
federation_models.Actor,
|
||||||
[
|
[
|
||||||
|
|
|
@ -370,7 +370,7 @@ def test_outbox_create_audio(factories, mocker):
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
expected = serializer.data
|
expected = serializer.data
|
||||||
expected["to"] = [{"type": "followers", "target": upload.library.actor}]
|
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||||
|
|
||||||
assert dict(activity["payload"]) == dict(expected)
|
assert dict(activity["payload"]) == dict(expected)
|
||||||
assert activity["actor"] == upload.library.actor
|
assert activity["actor"] == upload.library.actor
|
||||||
|
@ -685,7 +685,7 @@ def test_outbox_delete_audio(factories):
|
||||||
{"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
|
{"type": "Delete", "object": {"type": "Audio", "id": [upload.fid]}}
|
||||||
).data
|
).data
|
||||||
|
|
||||||
expected["to"] = [{"type": "followers", "target": upload.library.actor}]
|
expected["to"] = [{"type": "followers", "target": upload.library}]
|
||||||
|
|
||||||
assert dict(activity["payload"]) == dict(expected)
|
assert dict(activity["payload"]) == dict(expected)
|
||||||
assert activity["actor"] == upload.library.actor
|
assert activity["actor"] == upload.library.actor
|
||||||
|
|
|
@ -548,11 +548,13 @@ def test_music_library_serializer_to_ap(factories):
|
||||||
"type": "Library",
|
"type": "Library",
|
||||||
"id": library.fid,
|
"id": library.fid,
|
||||||
"name": library.name,
|
"name": library.name,
|
||||||
|
"summary": library.description,
|
||||||
"attributedTo": library.actor.fid,
|
"attributedTo": library.actor.fid,
|
||||||
"totalItems": 0,
|
"totalItems": 0,
|
||||||
"current": library.fid + "?page=1",
|
"current": library.fid + "?page=1",
|
||||||
"last": library.fid + "?page=1",
|
"last": library.fid + "?page=1",
|
||||||
"first": library.fid + "?page=1",
|
"first": library.fid + "?page=1",
|
||||||
|
"followers": library.followers_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
assert serializer.data == expected
|
assert serializer.data == expected
|
||||||
|
@ -567,8 +569,10 @@ def test_music_library_serializer_from_public(factories, mocker):
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
|
"summary": "World",
|
||||||
"type": "Library",
|
"type": "Library",
|
||||||
"id": "https://library.id",
|
"id": "https://library.id",
|
||||||
|
"followers": "https://library.id/followers",
|
||||||
"attributedTo": actor.fid,
|
"attributedTo": actor.fid,
|
||||||
"totalItems": 12,
|
"totalItems": 12,
|
||||||
"first": "https://library.id?page=1",
|
"first": "https://library.id?page=1",
|
||||||
|
@ -585,6 +589,8 @@ def test_music_library_serializer_from_public(factories, mocker):
|
||||||
assert library.uploads_count == data["totalItems"]
|
assert library.uploads_count == data["totalItems"]
|
||||||
assert library.privacy_level == "everyone"
|
assert library.privacy_level == "everyone"
|
||||||
assert library.name == "Hello"
|
assert library.name == "Hello"
|
||||||
|
assert library.description == "World"
|
||||||
|
assert library.followers_url == data["followers"]
|
||||||
|
|
||||||
retrieve.assert_called_once_with(
|
retrieve.assert_called_once_with(
|
||||||
actor.fid,
|
actor.fid,
|
||||||
|
@ -603,8 +609,10 @@ def test_music_library_serializer_from_private(factories, mocker):
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"audience": "",
|
"audience": "",
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
|
"summary": "World",
|
||||||
"type": "Library",
|
"type": "Library",
|
||||||
"id": "https://library.id",
|
"id": "https://library.id",
|
||||||
|
"followers": "https://library.id/followers",
|
||||||
"attributedTo": actor.fid,
|
"attributedTo": actor.fid,
|
||||||
"totalItems": 12,
|
"totalItems": 12,
|
||||||
"first": "https://library.id?page=1",
|
"first": "https://library.id?page=1",
|
||||||
|
@ -621,6 +629,8 @@ def test_music_library_serializer_from_private(factories, mocker):
|
||||||
assert library.uploads_count == data["totalItems"]
|
assert library.uploads_count == data["totalItems"]
|
||||||
assert library.privacy_level == "me"
|
assert library.privacy_level == "me"
|
||||||
assert library.name == "Hello"
|
assert library.name == "Hello"
|
||||||
|
assert library.description == "World"
|
||||||
|
assert library.followers_url == data["followers"]
|
||||||
retrieve.assert_called_once_with(
|
retrieve.assert_called_once_with(
|
||||||
actor.fid,
|
actor.fid,
|
||||||
actor=None,
|
actor=None,
|
||||||
|
@ -637,8 +647,10 @@ def test_music_library_serializer_from_ap_update(factories, mocker):
|
||||||
"@context": jsonld.get_default_context(),
|
"@context": jsonld.get_default_context(),
|
||||||
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
"audience": "https://www.w3.org/ns/activitystreams#Public",
|
||||||
"name": "Hello",
|
"name": "Hello",
|
||||||
|
"summary": "World",
|
||||||
"type": "Library",
|
"type": "Library",
|
||||||
"id": library.fid,
|
"id": library.fid,
|
||||||
|
"followers": "https://library.id/followers",
|
||||||
"attributedTo": actor.fid,
|
"attributedTo": actor.fid,
|
||||||
"totalItems": 12,
|
"totalItems": 12,
|
||||||
"first": "https://library.id?page=1",
|
"first": "https://library.id?page=1",
|
||||||
|
@ -654,6 +666,8 @@ def test_music_library_serializer_from_ap_update(factories, mocker):
|
||||||
assert library.uploads_count == data["totalItems"]
|
assert library.uploads_count == data["totalItems"]
|
||||||
assert library.privacy_level == "everyone"
|
assert library.privacy_level == "everyone"
|
||||||
assert library.name == "Hello"
|
assert library.name == "Hello"
|
||||||
|
assert library.description == "World"
|
||||||
|
assert library.followers_url == data["followers"]
|
||||||
|
|
||||||
|
|
||||||
def test_activity_pub_artist_serializer_to_ap(factories):
|
def test_activity_pub_artist_serializer_to_ap(factories):
|
||||||
|
|
|
@ -219,6 +219,7 @@ def test_music_library_retrieve_page_public(factories, api_client):
|
||||||
"actor": library.actor,
|
"actor": library.actor,
|
||||||
"page": Paginator([upload], 1).page(1),
|
"page": Paginator([upload], 1).page(1),
|
||||||
"name": library.name,
|
"name": library.name,
|
||||||
|
"summary": library.description,
|
||||||
}
|
}
|
||||||
).data
|
).data
|
||||||
|
|
||||||
|
|
|
@ -447,13 +447,16 @@ def test_manage_library_serializer(factories, now, to_api_date):
|
||||||
"fid": library.fid,
|
"fid": library.fid,
|
||||||
"url": library.url,
|
"url": library.url,
|
||||||
"uuid": str(library.uuid),
|
"uuid": str(library.uuid),
|
||||||
|
"followers_url": library.followers_url,
|
||||||
"domain": library.domain_name,
|
"domain": library.domain_name,
|
||||||
"is_local": library.is_local,
|
"is_local": library.is_local,
|
||||||
"name": library.name,
|
"name": library.name,
|
||||||
|
"description": library.description,
|
||||||
"privacy_level": library.privacy_level,
|
"privacy_level": library.privacy_level,
|
||||||
"creation_date": to_api_date(library.creation_date),
|
"creation_date": to_api_date(library.creation_date),
|
||||||
"actor": serializers.ManageBaseActorSerializer(library.actor).data,
|
"actor": serializers.ManageBaseActorSerializer(library.actor).data,
|
||||||
"uploads_count": 44,
|
"uploads_count": 44,
|
||||||
|
"followers_count": 42,
|
||||||
}
|
}
|
||||||
s = serializers.ManageLibrarySerializer(library)
|
s = serializers.ManageLibrarySerializer(library)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
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
|
# this test is commented since it's very slow, but it can be useful for future development
|
||||||
# def test_pytest_plugin_initial(migrator):
|
# def test_pytest_plugin_initial(migrator):
|
||||||
# mapping_list = [
|
# mapping_list = [
|
||||||
|
@ -77,107 +72,3 @@ 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].artist.pk == old_album.artist.pk
|
||||||
assert album_obj.artist_credit.all()[0].joinphrase == ""
|
assert album_obj.artist_credit.all()[0].joinphrase == ""
|
||||||
assert album_obj.artist_credit.all()[0].credit == old_album.artist.name
|
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()
|
now = timezone.now()
|
||||||
actor = factories["federation.Actor"]()
|
actor = factories["federation.Actor"]()
|
||||||
library = factories["music.Library"](
|
library = factories["music.Library"](
|
||||||
name="Hello world", actor=actor, privacy_level="instance"
|
name="Hello world", description="hello", actor=actor, privacy_level="instance"
|
||||||
)
|
)
|
||||||
|
|
||||||
assert library.creation_date >= now
|
assert library.creation_date >= now
|
||||||
|
|
|
@ -415,6 +415,31 @@ def test_track_upload_serializer(factories):
|
||||||
assert data == expected
|
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):
|
def test_upload_with_channel(factories, uploaded_audio_file):
|
||||||
channel = factories["audio.Channel"](attributed_to__local=True)
|
channel = factories["audio.Channel"](attributed_to__local=True)
|
||||||
user = channel.attributed_to.user
|
user = channel.attributed_to.user
|
||||||
|
|
|
@ -311,6 +311,7 @@ def test_library_library(spa_html, no_api_auth, client, factories, settings):
|
||||||
},
|
},
|
||||||
{"tag": "meta", "property": "og:type", "content": "website"},
|
{"tag": "meta", "property": "og:type", "content": "website"},
|
||||||
{"tag": "meta", "property": "og:title", "content": library.name},
|
{"tag": "meta", "property": "og:title", "content": library.name},
|
||||||
|
{"tag": "meta", "property": "og:description", "content": library.description},
|
||||||
{
|
{
|
||||||
"tag": "link",
|
"tag": "link",
|
||||||
"rel": "alternate",
|
"rel": "alternate",
|
||||||
|
|
|
@ -638,6 +638,25 @@ 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):
|
def test_user_can_list_their_library(factories, logged_in_api_client):
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
library = factories["music.Library"](actor=actor)
|
library = factories["music.Library"](actor=actor)
|
||||||
|
@ -691,7 +710,17 @@ def test_user_cannot_delete_other_actors_library(factories, logged_in_api_client
|
||||||
url = reverse("api:v1:libraries-detail", kwargs={"uuid": library.uuid})
|
url = reverse("api:v1:libraries-detail", kwargs={"uuid": library.uuid})
|
||||||
response = logged_in_api_client.delete(url)
|
response = logged_in_api_client.delete(url)
|
||||||
|
|
||||||
assert response.status_code == 405
|
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}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_user_cannot_get_other_not_playable_uploads(factories, logged_in_api_client):
|
def test_user_cannot_get_other_not_playable_uploads(factories, logged_in_api_client):
|
||||||
|
@ -906,6 +935,25 @@ def test_user_can_patch_draft_upload_status_triggers_processing(
|
||||||
m.assert_called_once_with(tasks.process_upload.delay, upload_id=upload.pk)
|
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"])
|
@pytest.mark.parametrize("entity", ["artist", "album", "track"])
|
||||||
def test_can_get_libraries_for_music_entities(
|
def test_can_get_libraries_for_music_entities(
|
||||||
factories, api_client, entity, preferences
|
factories, api_client, entity, preferences
|
||||||
|
@ -1565,25 +1613,3 @@ def test_album_create_artist_credit(factories, logged_in_api_client):
|
||||||
url, {"artist": artist.pk, "title": "super album"}, format="json"
|
url, {"artist": artist.pk, "title": "super album"}, format="json"
|
||||||
)
|
)
|
||||||
assert response.status_code == 204
|
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,30 +89,3 @@ def test_playlist_serializer(factories, to_api_date):
|
||||||
serializer = serializers.PlaylistSerializer(playlist)
|
serializer = serializers.PlaylistSerializer(playlist)
|
||||||
|
|
||||||
assert serializer.data == expected
|
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,7 +4,6 @@ import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.federation import utils as federation_utils
|
from funkwhale_api.federation import utils as federation_utils
|
||||||
from funkwhale_api.music import models as music_models
|
|
||||||
from funkwhale_api.users import models
|
from funkwhale_api.users import models
|
||||||
|
|
||||||
|
|
||||||
|
@ -183,18 +182,6 @@ 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):
|
def test_get_channels_groups(factories):
|
||||||
user = factories["users.User"](permission_library=True)
|
user = factories["users.User"](permission_library=True)
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
Drop libraries in favor of playlist (#2366)
|
|
|
@ -33,6 +33,7 @@ services:
|
||||||
retries: 3
|
retries: 3
|
||||||
command: >
|
command: >
|
||||||
sh -c "
|
sh -c "
|
||||||
|
funkwhale-manage migrate &&
|
||||||
funkwhale-manage collectstatic --no-input &&
|
funkwhale-manage collectstatic --no-input &&
|
||||||
uvicorn --reload config.asgi:application --host 0.0.0.0 --port 5000 --reload-dir config/ --reload-dir funkwhale_api/
|
uvicorn --reload config.asgi:application --host 0.0.0.0 --port 5000 --reload-dir config/ --reload-dir funkwhale_api/
|
||||||
"
|
"
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
# 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