Compare commits

...

2 Commits

8 changed files with 414 additions and 46 deletions

View File

@ -1,5 +1,7 @@
from rest_framework import serializers from rest_framework import serializers
from akarpov.utils.models import get_model_user_field
class RecursiveField(serializers.Serializer): class RecursiveField(serializers.Serializer):
def to_representation(self, value): def to_representation(self, value):
@ -9,6 +11,19 @@ def to_representation(self, value):
class SetUserModelSerializer(serializers.ModelSerializer): class SetUserModelSerializer(serializers.ModelSerializer):
def create(self, validated_data): def create(self, validated_data):
creator = self.context["request"].user if self.context["request"].user.is_authenticated:
obj = self.Meta.model.objects.create(creator=creator, **validated_data) creator = self.context["request"].user
else:
creator = None
validated_data[
get_model_user_field(
self.Meta.model._meta.app_label, self.Meta.model._meta.model_name
)
] = creator
try:
obj = self.Meta.model.objects.create(**validated_data)
except TypeError:
raise serializers.ValidationError(
{"detail": "You need to login to create this object"}
)
return obj return obj

View File

@ -0,0 +1,9 @@
from django_filters import rest_framework as filters
from akarpov.music.models import Playlist
class PlaylistFilter(filters.FilterSet):
class Meta:
model = Playlist
fields = ()

View File

@ -2,37 +2,40 @@
from rest_framework import serializers from rest_framework import serializers
from akarpov.common.api.serializers import SetUserModelSerializer from akarpov.common.api.serializers import SetUserModelSerializer
from akarpov.music.models import Album, Author, Playlist, Song from akarpov.music.models import (
Album,
Author,
Playlist,
PlaylistSong,
Song,
SongUserRating,
)
from akarpov.users.api.serializers import UserPublicInfoSerializer from akarpov.users.api.serializers import UserPublicInfoSerializer
class AuthorSerializer(serializers.ModelSerializer): class AuthorSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
@extend_schema_field(serializers.URLField)
def get_url(self, obj):
return obj.get_absolute_url()
class Meta: class Meta:
model = Author model = Author
fields = ["name", "link", "image_cropped", "url"] fields = ["name", "slug", "link", "image_cropped"]
class AlbumSerializer(serializers.ModelSerializer): class AlbumSerializer(serializers.ModelSerializer):
url = serializers.SerializerMethodField(method_name="get_url")
@extend_schema_field(serializers.URLField)
def get_url(self, obj):
return obj.get_absolute_url()
class Meta: class Meta:
model = Album model = Album
fields = ["name", "link", "image_cropped", "url"] fields = ["name", "slug", "link", "image_cropped"]
class SongSerializer(serializers.ModelSerializer): class SongSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True) authors = AuthorSerializer(many=True)
album = AlbumSerializer() album = AlbumSerializer()
liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
def get_liked(self, obj):
if self.context["request"].user.is_authenticated:
return SongUserRating.objects.filter(
song=obj, user=self.context["request"].user, like=True
).exists()
class Meta: class Meta:
model = Song model = Song
@ -45,6 +48,7 @@ class Meta:
"file", "file",
"authors", "authors",
"album", "album",
"liked",
] ]
extra_kwargs = { extra_kwargs = {
"slug": {"read_only": True}, "slug": {"read_only": True},
@ -58,12 +62,15 @@ class ListSongSerializer(SetUserModelSerializer):
album = serializers.CharField(source="album.name", read_only=True) album = serializers.CharField(source="album.name", read_only=True)
liked = serializers.SerializerMethodField(method_name="get_liked") liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
def get_liked(self, obj): def get_liked(self, obj):
if "likes" in self.context:
return self.context["likes"]
return obj.id in self.context["likes_ids"] return obj.id in self.context["likes_ids"]
class Meta: class Meta:
model = Song model = Song
fields = ["name", "slug", "file", "image_cropped", "length", "album"] fields = ["name", "slug", "file", "image_cropped", "length", "album", "liked"]
extra_kwargs = { extra_kwargs = {
"slug": {"read_only": True}, "slug": {"read_only": True},
"image_cropped": {"read_only": True}, "image_cropped": {"read_only": True},
@ -73,14 +80,15 @@ class Meta:
class PlaylistSerializer(SetUserModelSerializer): class PlaylistSerializer(SetUserModelSerializer):
creator = UserPublicInfoSerializer() creator = UserPublicInfoSerializer(read_only=True)
class Meta: class Meta:
model = Playlist model = Playlist
fields = ["name", "slug", "private", "creator"] fields = ["name", "length", "slug", "private", "creator"]
extra_kwargs = { extra_kwargs = {
"slug": {"read_only": True}, "slug": {"read_only": True},
"creator": {"read_only": True}, "creator": {"read_only": True},
"length": {"read_only": True},
} }
@ -95,3 +103,131 @@ class Meta:
"slug": {"read_only": True}, "slug": {"read_only": True},
"creator": {"read_only": True}, "creator": {"read_only": True},
} }
class AddSongToPlaylistSerializer(serializers.ModelSerializer):
song = serializers.SlugField()
playlist = serializers.SlugField()
class Meta:
model = Playlist
fields = ["song", "playlist"]
extra_kwargs = {
"song": {"write_only": True},
"playlist": {"write_only": True},
}
def validate(self, attrs):
if not Playlist.objects.filter(slug=attrs["playlist"]).exists():
raise serializers.ValidationError("Playlist not found")
if not Song.objects.filter(slug=attrs["song"]).exists():
raise serializers.ValidationError("Song not found")
if self.context["create"]:
if PlaylistSong.objects.filter(
playlist__slug=attrs["playlist"], song__slug=attrs["song"]
).exists():
raise serializers.ValidationError("Song already in playlist")
else:
if not PlaylistSong.objects.filter(
playlist__slug=attrs["playlist"], song__slug=attrs["song"]
).exists():
raise serializers.ValidationError("Song not in playlist")
return attrs
def create(self, validated_data):
playlist = Playlist.objects.get(slug=validated_data["playlist"])
song = Song.objects.get(slug=validated_data["song"])
if not self.context["create"]:
playlist_song = PlaylistSong.objects.get(
playlist=playlist, song=song
).delete()
else:
order = playlist.songs.count()
playlist_song = playlist.songs.create(song=song, order=order)
return playlist_song
class LikeDislikeSongSerializer(serializers.ModelSerializer):
song = serializers.SlugField()
class Meta:
model = SongUserRating
fields = ["song"]
extra_kwargs = {
"song": {"write_only": True},
}
def validate(self, attrs):
if not Song.objects.filter(slug=attrs["song"]).exists():
raise serializers.ValidationError("Song not found")
return attrs
def create(self, validated_data):
song = Song.objects.get(slug=validated_data["song"])
if self.context["like"]:
if SongUserRating.objects.filter(
song=song, user=self.context["request"].user
).exists():
song_user_rating = SongUserRating.objects.get(
song=song, user=self.context["request"].user
)
if song_user_rating.like:
song_user_rating.delete()
else:
song_user_rating.like = True
song_user_rating.save()
else:
song_user_rating = SongUserRating.objects.create(
song=song, user=self.context["request"].user, like=True
)
else:
if SongUserRating.objects.filter(
song=song, user=self.context["request"].user
).exists():
song_user_rating = SongUserRating.objects.get(
song=song, user=self.context["request"].user
)
if not song_user_rating.like:
song_user_rating.delete()
else:
song_user_rating.like = False
song_user_rating.save()
else:
song_user_rating = SongUserRating.objects.create(
song=song, user=self.context["request"].user, like=False
)
return song_user_rating
class ListPlaylistSerializer(serializers.ModelSerializer):
class Meta:
model = Playlist
fields = ["name", "slug", "private"]
extra_kwargs = {
"slug": {"read_only": True},
"private": {"read_only": True},
}
class FullAlbumSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
}
class FullAuthorSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
class Meta:
model = Author
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
}

View File

@ -1,8 +1,19 @@
from django.urls import path from django.urls import path
from akarpov.music.api.views import ( from akarpov.music.api.views import (
AddSongToPlaylistAPIView,
DislikeSongAPIView,
LikeSongAPIView,
ListAlbumsAPIView,
ListAuthorsAPIView,
ListCreatePlaylistAPIView, ListCreatePlaylistAPIView,
ListCreateSongAPIView, ListCreateSongAPIView,
ListDislikedSongsAPIView,
ListLikedSongsAPIView,
ListPublicPlaylistAPIView,
RemoveSongFromPlaylistAPIView,
RetrieveUpdateDestroyAlbumAPIView,
RetrieveUpdateDestroyAuthorAPIView,
RetrieveUpdateDestroyPlaylistAPIView, RetrieveUpdateDestroyPlaylistAPIView,
RetrieveUpdateDestroySongAPIView, RetrieveUpdateDestroySongAPIView,
) )
@ -10,9 +21,16 @@
app_name = "music" app_name = "music"
urlpatterns = [ urlpatterns = [
path("song/liked/", ListLikedSongsAPIView.as_view(), name="list_liked"),
path("song/disliked/", ListDislikedSongsAPIView.as_view(), name="list_disliked"),
path( path(
"playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist" "playlists/", ListCreatePlaylistAPIView.as_view(), name="list_create_playlist"
), ),
path(
"playlists/public/",
ListPublicPlaylistAPIView.as_view(),
name="list_public",
),
path( path(
"playlists/<str:slug>", "playlists/<str:slug>",
RetrieveUpdateDestroyPlaylistAPIView.as_view(), RetrieveUpdateDestroyPlaylistAPIView.as_view(),
@ -24,4 +42,20 @@
RetrieveUpdateDestroySongAPIView.as_view(), RetrieveUpdateDestroySongAPIView.as_view(),
name="retrieve_update_delete_song", name="retrieve_update_delete_song",
), ),
path("song/like/", LikeSongAPIView.as_view()),
path("song/dislike/", DislikeSongAPIView.as_view()),
path("playlists/add/", AddSongToPlaylistAPIView.as_view()),
path("playlists/remove/", RemoveSongFromPlaylistAPIView.as_view()),
path("albums/", ListAlbumsAPIView.as_view(), name="list_albums"),
path(
"albums/<str:slug>",
RetrieveUpdateDestroyAlbumAPIView.as_view(),
name="retrieve_update_delete_album",
),
path("authors/", ListAuthorsAPIView.as_view(), name="list_authors"),
path(
"authors/<str:slug>",
RetrieveUpdateDestroyAuthorAPIView.as_view(),
name="retrieve_update_delete_author",
),
] ]

View File

@ -3,12 +3,19 @@
from akarpov.common.api.pagination import StandardResultsSetPagination from akarpov.common.api.pagination import StandardResultsSetPagination
from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly
from akarpov.music.api.serializers import ( from akarpov.music.api.serializers import (
AddSongToPlaylistSerializer,
AlbumSerializer,
AuthorSerializer,
FullAlbumSerializer,
FullAuthorSerializer,
FullPlaylistSerializer, FullPlaylistSerializer,
LikeDislikeSongSerializer,
ListPlaylistSerializer,
ListSongSerializer, ListSongSerializer,
PlaylistSerializer, PlaylistSerializer,
SongSerializer, SongSerializer,
) )
from akarpov.music.models import Playlist, Song, SongUserRating from akarpov.music.models import Album, Author, Playlist, Song, SongUserRating
class LikedSongsContextMixin(generics.GenericAPIView): class LikedSongsContextMixin(generics.GenericAPIView):
@ -26,11 +33,23 @@ def get_serializer_context(self):
class ListCreatePlaylistAPIView(generics.ListCreateAPIView): class ListCreatePlaylistAPIView(generics.ListCreateAPIView):
permission_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticatedOrReadOnly]
serializer_class = PlaylistSerializer serializer_class = PlaylistSerializer
def get_queryset(self): def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user) if self.request.user.is_authenticated:
return Playlist.objects.filter(creator=self.request.user).select_related(
"creator"
)
return Playlist.objects.filter(private=False).select_related("creator")
class ListPublicPlaylistAPIView(generics.ListAPIView):
permission_classes = [permissions.AllowAny]
serializer_class = PlaylistSerializer
def get_queryset(self):
return Playlist.objects.filter(private=False)
class RetrieveUpdateDestroyPlaylistAPIView( class RetrieveUpdateDestroyPlaylistAPIView(
@ -41,14 +60,11 @@ class RetrieveUpdateDestroyPlaylistAPIView(
permission_classes = [IsCreatorOrReadOnly] permission_classes = [IsCreatorOrReadOnly]
serializer_class = FullPlaylistSerializer serializer_class = FullPlaylistSerializer
def __init__(self, **kwargs): def get_queryset(self):
super().__init__(**kwargs) qs = Playlist.objects.filter(private=False)
self.object = None if self.request.user.is_authenticated:
qs = Playlist.objects.filter(creator=self.request.user) | qs
def get_object(self): return qs.select_related("creator")
if not self.object:
self.object = super().get_object()
return self.object
class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView): class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
@ -59,9 +75,11 @@ class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
def get_queryset(self): def get_queryset(self):
if self.request.user.is_authenticated: if self.request.user.is_authenticated:
return ( return (
Song.objects.exclude( Song.objects.all()
.exclude(
id__in=SongUserRating.objects.filter( id__in=SongUserRating.objects.filter(
user=self.request.user user=self.request.user,
like=False,
).values_list("song_id", flat=True) ).values_list("song_id", flat=True)
) )
.prefetch_related("authors") .prefetch_related("authors")
@ -76,29 +94,149 @@ class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
permission_classes = [IsCreatorOrReadOnly] permission_classes = [IsCreatorOrReadOnly]
serializer_class = SongSerializer serializer_class = SongSerializer
def __init__(self, **kwargs): def get_queryset(self):
super().__init__(**kwargs) return Song.objects.all()
self.object = None
def get_object(self):
if not self.object: class ListSongPlaylistsAPIView(generics.ListAPIView):
self.object = super().get_object() serializer_class = ListPlaylistSerializer
return self.object permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(
songs__song__slug=self.kwargs["slug"], creator=self.request.user
)
class ListLikedSongsAPIView(generics.ListAPIView): class ListLikedSongsAPIView(generics.ListAPIView):
serializer_class = ListSongSerializer serializer_class = ListSongSerializer
pagination_class = StandardResultsSetPagination pagination_class = StandardResultsSetPagination
authentication_classes = [permissions.IsAuthenticated] permission_classes = [permissions.IsAuthenticated]
def get_serializer_context(self):
context = super().get_serializer_context()
context["likes"] = True
return context
def get_queryset(self): def get_queryset(self):
return ( return (
Song.objects.cache() Song.objects.cache()
.filter( .filter(
id__in=self.request.user.song_likes.objects.cache() id__in=SongUserRating.objects.cache()
.all() .filter(user=self.request.user, like=True)
.values_list("song_id", flat=True) .values_list("song_id", flat=True)
) )
.prefetch_related("authors") .prefetch_related("authors")
.select_related("album") .select_related("album")
) )
class ListDislikedSongsAPIView(generics.ListAPIView):
serializer_class = ListSongSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.IsAuthenticated]
def get_serializer_context(self):
context = super().get_serializer_context()
context["likes"] = False
return context
def get_queryset(self):
return (
Song.objects.cache()
.filter(
id__in=SongUserRating.objects.cache()
.filter(user=self.request.user, like=True)
.values_list("song_id", flat=False)
)
.prefetch_related("authors")
.select_related("album")
)
class AddSongToPlaylistAPIView(generics.CreateAPIView):
serializer_class = AddSongToPlaylistSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user)
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["create"] = True
return context
class RemoveSongFromPlaylistAPIView(generics.DestroyAPIView):
serializer_class = AddSongToPlaylistSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Playlist.objects.filter(creator=self.request.user)
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["create"] = False
return context
class LikeSongAPIView(generics.CreateAPIView):
serializer_class = LikeDislikeSongSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return SongUserRating.objects.all()
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["like"] = True
return context
class DislikeSongAPIView(generics.CreateAPIView):
serializer_class = LikeDislikeSongSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return SongUserRating.objects.all()
def get_serializer_context(self, **kwargs):
context = super().get_serializer_context()
context["like"] = False
return context
class ListAlbumsAPIView(generics.ListAPIView):
serializer_class = AlbumSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Album.objects.all()
class RetrieveUpdateDestroyAlbumAPIView(
LikedSongsContextMixin, generics.RetrieveUpdateDestroyAPIView
):
lookup_field = "slug"
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
serializer_class = FullAlbumSerializer
class ListAuthorsAPIView(generics.ListAPIView):
serializer_class = AuthorSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
def get_queryset(self):
return Author.objects.all()
class RetrieveUpdateDestroyAuthorAPIView(
LikedSongsContextMixin, generics.RetrieveUpdateDestroyAPIView
):
lookup_field = "slug"
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
serializer_class = FullAuthorSerializer

View File

@ -87,14 +87,14 @@ class SlugMeta:
class Playlist(ShortLinkModel, UserHistoryModel): class Playlist(ShortLinkModel, UserHistoryModel):
name = models.CharField(max_length=200) name = models.CharField(max_length=200)
private = models.BooleanField(default=False) private = models.BooleanField(default=True)
creator = models.ForeignKey( creator = models.ForeignKey(
"users.User", related_name="playlists", on_delete=models.CASCADE "users.User", related_name="playlists", on_delete=models.CASCADE
) )
length = models.IntegerField(default=0) length = models.IntegerField(default=0)
def get_absolute_url(self): def get_absolute_url(self):
return reverse("playlist:song", kwargs={"slug": self.slug}) return reverse("music:playlist", kwargs={"slug": self.slug})
def get_songs(self): def get_songs(self):
return self.songs.all().values("song") return self.songs.all().values("song")

View File

@ -3,7 +3,7 @@
from django.db.models.signals import post_delete, post_save, pre_save from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from akarpov.music.models import Song, SongUserRating from akarpov.music.models import PlaylistSong, Song, SongUserRating
@receiver(post_delete, sender=Song) @receiver(post_delete, sender=Song)
@ -34,3 +34,27 @@ def create_or_update_rating(sender, instance: SongUserRating, **kwargs):
else: else:
song.likes -= 1 song.likes -= 1
song.save(update_fields=["likes"]) song.save(update_fields=["likes"])
@receiver(post_delete, sender=SongUserRating)
def delete_rating(sender, instance: SongUserRating, **kwargs):
song = instance.song
if instance.like:
song.likes -= 1
else:
song.likes += 1
song.save(update_fields=["likes"])
@receiver(post_save, sender=PlaylistSong)
def update_playlist_length(sender, instance: PlaylistSong, **kwargs):
playlist = instance.playlist
playlist.length = playlist.songs.count()
playlist.save(update_fields=["length"])
@receiver(post_delete, sender=PlaylistSong)
def update_playlist_length_delete(sender, instance: PlaylistSong, **kwargs):
playlist = instance.playlist
playlist.length = playlist.songs.count()
playlist.save(update_fields=["length"])

View File

@ -26,6 +26,18 @@ def get_object_user(obj: Model) -> User | None:
return None return None
@lru_cache
def get_model_user_field(app_name: str, model: str) -> str:
model = apps.get_model(app_name, model)
if hasattr(model, "creator") and model.creator.field.related_model is User:
return "creator"
elif hasattr(model, "user") and model.user.field.related_model is User:
return "user"
elif hasattr(model, "owner") and model.owner.field.related_model is User:
return "owner"
return ""
@lru_cache @lru_cache
def get_app_verbose_name(app: str) -> str: def get_app_verbose_name(app: str) -> str:
return apps.get_app_config(app).verbose_name return apps.get_app_config(app).verbose_name