mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-25 18:43:47 +03:00
major music updates, minor template fixes
This commit is contained in:
parent
fdfe839d19
commit
127b4b6e11
|
@ -1,9 +1,17 @@
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from akarpov.music.models import Album, Author, Playlist, PlaylistSong, Song
|
from akarpov.music.models import (
|
||||||
|
Album,
|
||||||
|
Author,
|
||||||
|
Playlist,
|
||||||
|
PlaylistSong,
|
||||||
|
Song,
|
||||||
|
SongUserRating,
|
||||||
|
)
|
||||||
|
|
||||||
admin.site.register(Author)
|
admin.site.register(Author)
|
||||||
admin.site.register(Album)
|
admin.site.register(Album)
|
||||||
admin.site.register(Song)
|
admin.site.register(Song)
|
||||||
admin.site.register(Playlist)
|
admin.site.register(Playlist)
|
||||||
admin.site.register(PlaylistSong)
|
admin.site.register(PlaylistSong)
|
||||||
|
admin.site.register(SongUserRating)
|
||||||
|
|
|
@ -32,7 +32,7 @@ class SongSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
@extend_schema_field(serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
def get_liked(self, obj):
|
def get_liked(self, obj):
|
||||||
if "request" in self.context:
|
if "request" in self.context and self.context["request"]:
|
||||||
if self.context["request"].user.is_authenticated:
|
if self.context["request"].user.is_authenticated:
|
||||||
return SongUserRating.objects.filter(
|
return SongUserRating.objects.filter(
|
||||||
song=obj, user=self.context["request"].user, like=True
|
song=obj, user=self.context["request"].user, like=True
|
||||||
|
@ -61,8 +61,8 @@ class Meta:
|
||||||
|
|
||||||
|
|
||||||
class ListSongSerializer(SetUserModelSerializer):
|
class ListSongSerializer(SetUserModelSerializer):
|
||||||
album = AlbumSerializer(read_only=True)
|
album = serializers.SerializerMethodField(method_name="get_album")
|
||||||
authors = AuthorSerializer(many=True, read_only=True)
|
authors = serializers.SerializerMethodField(method_name="get_authors")
|
||||||
liked = serializers.SerializerMethodField(method_name="get_liked")
|
liked = serializers.SerializerMethodField(method_name="get_liked")
|
||||||
|
|
||||||
@extend_schema_field(serializers.BooleanField)
|
@extend_schema_field(serializers.BooleanField)
|
||||||
|
@ -73,6 +73,20 @@ def get_liked(self, obj):
|
||||||
return obj.id in self.context["likes_ids"]
|
return obj.id in self.context["likes_ids"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@extend_schema_field(AlbumSerializer)
|
||||||
|
def get_album(self, obj):
|
||||||
|
if obj.album:
|
||||||
|
return AlbumSerializer(Album.objects.cache().get(id=obj.album_id)).data
|
||||||
|
return None
|
||||||
|
|
||||||
|
@extend_schema_field(AuthorSerializer(many=True))
|
||||||
|
def get_authors(self, obj):
|
||||||
|
if obj.authors:
|
||||||
|
return AuthorSerializer(
|
||||||
|
Author.objects.cache().filter(songs__id=obj.id), many=True
|
||||||
|
).data
|
||||||
|
return None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Song
|
model = Song
|
||||||
fields = [
|
fields = [
|
||||||
|
|
|
@ -9,8 +9,11 @@
|
||||||
ListCreatePlaylistAPIView,
|
ListCreatePlaylistAPIView,
|
||||||
ListCreateSongAPIView,
|
ListCreateSongAPIView,
|
||||||
ListDislikedSongsAPIView,
|
ListDislikedSongsAPIView,
|
||||||
|
ListenSongAPIView,
|
||||||
ListLikedSongsAPIView,
|
ListLikedSongsAPIView,
|
||||||
ListPublicPlaylistAPIView,
|
ListPublicPlaylistAPIView,
|
||||||
|
ListSongPlaylistsAPIView,
|
||||||
|
ListUserListenedSongsAPIView,
|
||||||
RemoveSongFromPlaylistAPIView,
|
RemoveSongFromPlaylistAPIView,
|
||||||
RetrieveUpdateDestroyAlbumAPIView,
|
RetrieveUpdateDestroyAlbumAPIView,
|
||||||
RetrieveUpdateDestroyAuthorAPIView,
|
RetrieveUpdateDestroyAuthorAPIView,
|
||||||
|
@ -42,10 +45,25 @@
|
||||||
RetrieveUpdateDestroySongAPIView.as_view(),
|
RetrieveUpdateDestroySongAPIView.as_view(),
|
||||||
name="retrieve_update_delete_song",
|
name="retrieve_update_delete_song",
|
||||||
),
|
),
|
||||||
path("song/like/", LikeSongAPIView.as_view()),
|
path(
|
||||||
path("song/dislike/", DislikeSongAPIView.as_view()),
|
"song/<str:slug>/playlists/",
|
||||||
path("playlists/add/", AddSongToPlaylistAPIView.as_view()),
|
ListSongPlaylistsAPIView.as_view(),
|
||||||
path("playlists/remove/", RemoveSongFromPlaylistAPIView.as_view()),
|
name="list_song_playlists",
|
||||||
|
),
|
||||||
|
path("song/listen/", ListenSongAPIView.as_view(), name="listen-song"),
|
||||||
|
path("song/listened/", ListUserListenedSongsAPIView.as_view(), name="listened"),
|
||||||
|
path("song/like/", LikeSongAPIView.as_view(), name="like-song"),
|
||||||
|
path("song/dislike/", DislikeSongAPIView.as_view(), name="dislike-song"),
|
||||||
|
path(
|
||||||
|
"playlists/add/",
|
||||||
|
AddSongToPlaylistAPIView.as_view(),
|
||||||
|
name="add-song-to-playlists",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"playlists/remove/",
|
||||||
|
RemoveSongFromPlaylistAPIView.as_view(),
|
||||||
|
name="playlists-remove",
|
||||||
|
),
|
||||||
path("albums/", ListAlbumsAPIView.as_view(), name="list_albums"),
|
path("albums/", ListAlbumsAPIView.as_view(), name="list_albums"),
|
||||||
path(
|
path(
|
||||||
"albums/<str:slug>",
|
"albums/<str:slug>",
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
|
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema
|
||||||
from rest_framework import generics, permissions
|
from rest_framework import generics, permissions
|
||||||
|
from rest_framework.response import Response
|
||||||
|
|
||||||
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
|
||||||
|
@ -15,7 +17,15 @@
|
||||||
PlaylistSerializer,
|
PlaylistSerializer,
|
||||||
SongSerializer,
|
SongSerializer,
|
||||||
)
|
)
|
||||||
from akarpov.music.models import Album, Author, Playlist, Song, SongUserRating
|
from akarpov.music.models import (
|
||||||
|
Album,
|
||||||
|
Author,
|
||||||
|
Playlist,
|
||||||
|
Song,
|
||||||
|
SongUserRating,
|
||||||
|
UserListenHistory,
|
||||||
|
)
|
||||||
|
from akarpov.music.tasks import listen_to_song
|
||||||
|
|
||||||
|
|
||||||
class LikedSongsContextMixin(generics.GenericAPIView):
|
class LikedSongsContextMixin(generics.GenericAPIView):
|
||||||
|
@ -73,19 +83,76 @@ class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
|
||||||
pagination_class = StandardResultsSetPagination
|
pagination_class = StandardResultsSetPagination
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
|
qs = Song.objects.cache()
|
||||||
|
|
||||||
|
if "sort" in self.request.query_params:
|
||||||
|
sorts = self.request.query_params["sort"].split(",")
|
||||||
|
for sort in sorts:
|
||||||
|
pref = "-"
|
||||||
|
if sort.startswith("-"):
|
||||||
|
pref = ""
|
||||||
|
if sort == "likes":
|
||||||
|
qs = qs.order_by(pref + "likes")
|
||||||
|
elif sort == "length":
|
||||||
|
qs = qs.order_by(pref + "length")
|
||||||
|
elif sort == "played":
|
||||||
|
qs = qs.order_by(pref + "played")
|
||||||
|
elif sort == "uploaded":
|
||||||
|
qs = qs.order_by(pref + "created")
|
||||||
|
|
||||||
if self.request.user.is_authenticated:
|
if self.request.user.is_authenticated:
|
||||||
return (
|
return qs.exclude(
|
||||||
Song.objects.all()
|
id__in=SongUserRating.objects.filter(
|
||||||
.exclude(
|
user=self.request.user,
|
||||||
id__in=SongUserRating.objects.filter(
|
like=False,
|
||||||
user=self.request.user,
|
).values_list("song_id", flat=True)
|
||||||
like=False,
|
|
||||||
).values_list("song_id", flat=True)
|
|
||||||
)
|
|
||||||
.prefetch_related("authors")
|
|
||||||
.select_related("album")
|
|
||||||
)
|
)
|
||||||
return Song.objects.all().prefetch_related("authors").select_related("album")
|
return qs
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="sort",
|
||||||
|
description="Sorting algorithm",
|
||||||
|
required=False,
|
||||||
|
type=str,
|
||||||
|
examples=[
|
||||||
|
OpenApiExample(
|
||||||
|
"Default",
|
||||||
|
description="by date added",
|
||||||
|
value=None,
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"played",
|
||||||
|
description="by total times played",
|
||||||
|
value="played",
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"likes",
|
||||||
|
description="by total likes",
|
||||||
|
value="likes",
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"likes reversed",
|
||||||
|
description="by total likes",
|
||||||
|
value="-likes",
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"length",
|
||||||
|
description="by track length",
|
||||||
|
value="length",
|
||||||
|
),
|
||||||
|
OpenApiExample(
|
||||||
|
"uploaded",
|
||||||
|
description="by date uploaded",
|
||||||
|
value="uploaded",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
def get(self, request, *args, **kwargs):
|
||||||
|
return self.list(request, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
|
class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
|
||||||
|
@ -142,15 +209,10 @@ def get_serializer_context(self):
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return (
|
return Song.objects.cache().filter(
|
||||||
Song.objects.cache()
|
id__in=SongUserRating.objects.cache()
|
||||||
.filter(
|
.filter(user=self.request.user, like=True)
|
||||||
id__in=SongUserRating.objects.cache()
|
.values_list("song_id", flat=False)
|
||||||
.filter(user=self.request.user, like=True)
|
|
||||||
.values_list("song_id", flat=False)
|
|
||||||
)
|
|
||||||
.prefetch_related("authors")
|
|
||||||
.select_related("album")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -240,3 +302,45 @@ class RetrieveUpdateDestroyAuthorAPIView(
|
||||||
lookup_url_kwarg = "slug"
|
lookup_url_kwarg = "slug"
|
||||||
permission_classes = [IsAdminOrReadOnly]
|
permission_classes = [IsAdminOrReadOnly]
|
||||||
serializer_class = FullAuthorSerializer
|
serializer_class = FullAuthorSerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ListenSongAPIView(generics.GenericAPIView):
|
||||||
|
serializer_class = LikeDislikeSongSerializer
|
||||||
|
permission_classes = [permissions.AllowAny]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Song.objects.cache()
|
||||||
|
|
||||||
|
def post(self, request, *args, **kwargs):
|
||||||
|
serializer = self.get_serializer(data=request.data)
|
||||||
|
serializer.is_valid(raise_exception=False)
|
||||||
|
data = serializer.validated_data
|
||||||
|
|
||||||
|
try:
|
||||||
|
song = Song.objects.cache().get(slug=data["song"])
|
||||||
|
except Song.DoesNotExist:
|
||||||
|
return Response(status=404)
|
||||||
|
if self.request.user.is_authenticated:
|
||||||
|
listen_to_song.apply_async(
|
||||||
|
kwargs={"song_id": song.id, "user_id": self.request.user.id},
|
||||||
|
countdown=2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
listen_to_song.apply_async(
|
||||||
|
kwargs={"song_id": song.id},
|
||||||
|
countdown=2,
|
||||||
|
)
|
||||||
|
return Response(status=201)
|
||||||
|
|
||||||
|
|
||||||
|
class ListUserListenedSongsAPIView(generics.ListAPIView):
|
||||||
|
serializer_class = ListSongSerializer
|
||||||
|
pagination_class = StandardResultsSetPagination
|
||||||
|
permission_classes = [permissions.IsAuthenticated]
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return Song.objects.cache().filter(
|
||||||
|
id__in=UserListenHistory.objects.cache()
|
||||||
|
.filter(user=self.request.user)
|
||||||
|
.values_list("song_id", flat=True)
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
# Generated by Django 4.2.7 on 2023-12-16 17:53
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("music", "0010_song_likes_songuserrating"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="playlist",
|
||||||
|
name="private",
|
||||||
|
field=models.BooleanField(default=True),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="UserListenHistory",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
verbose_name="ID",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"song",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="listeners",
|
||||||
|
to="music.song",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="songs_listened",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"ordering": ["-created"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
|
@ -147,3 +147,14 @@ def __str__(self):
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ["song", "user"]
|
unique_together = ["song", "user"]
|
||||||
ordering = ["-created"]
|
ordering = ["-created"]
|
||||||
|
|
||||||
|
|
||||||
|
class UserListenHistory(models.Model):
|
||||||
|
user = models.ForeignKey(
|
||||||
|
"users.User", related_name="songs_listened", on_delete=models.CASCADE
|
||||||
|
)
|
||||||
|
song = models.ForeignKey("Song", related_name="listeners", on_delete=models.CASCADE)
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
ordering = ["-created"]
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
from asgiref.sync import async_to_sync
|
from asgiref.sync import async_to_sync
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from channels.layers import get_channel_layer
|
from channels.layers import get_channel_layer
|
||||||
|
from django.utils.timezone import now
|
||||||
from pytube import Channel, Playlist
|
from pytube import Channel, Playlist
|
||||||
|
|
||||||
from akarpov.music.api.serializers import SongSerializer
|
from akarpov.music.api.serializers import SongSerializer
|
||||||
from akarpov.music.models import RadioSong, Song
|
from akarpov.music.models import RadioSong, Song, UserListenHistory
|
||||||
from akarpov.music.services import yandex, youtube
|
from akarpov.music.services import yandex, youtube
|
||||||
from akarpov.music.services.file import load_dir, load_file
|
from akarpov.music.services.file import load_dir, load_file
|
||||||
from akarpov.utils.celery import get_scheduled_tasks_name
|
from akarpov.utils.celery import get_scheduled_tasks_name
|
||||||
|
@ -86,3 +87,29 @@ def start_next_song(previous_ids: list):
|
||||||
countdown=song.length,
|
countdown=song.length,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@shared_task
|
||||||
|
def listen_to_song(song_id, user_id=None):
|
||||||
|
# protection from multiple listen,
|
||||||
|
# check that last listen by user was more than the length of the song
|
||||||
|
# and last listened song is not the same
|
||||||
|
s = Song.objects.get(id=song_id)
|
||||||
|
s.played += 1
|
||||||
|
s.save(update_fields=["played"])
|
||||||
|
if user_id:
|
||||||
|
try:
|
||||||
|
last_listen = UserListenHistory.objects.filter(user_id=user_id).latest("id")
|
||||||
|
except UserListenHistory.DoesNotExist:
|
||||||
|
last_listen = None
|
||||||
|
if (
|
||||||
|
last_listen
|
||||||
|
and last_listen.song_id == song_id
|
||||||
|
or last_listen.created + s.length > now()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
UserListenHistory.objects.create(
|
||||||
|
user_id=user_id,
|
||||||
|
song_id=song_id,
|
||||||
|
)
|
||||||
|
return song_id
|
||||||
|
|
|
@ -159,6 +159,35 @@
|
||||||
const toastContainer = document.getElementById('toastContainer')
|
const toastContainer = document.getElementById('toastContainer')
|
||||||
|
|
||||||
|
|
||||||
|
function sleep(ms) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
function timeSince(date) {
|
||||||
|
let seconds = Math.floor((new Date() - date) / 1000);
|
||||||
|
let interval = seconds / 31536000;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " years";
|
||||||
|
}
|
||||||
|
interval = seconds / 2592000;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " months";
|
||||||
|
}
|
||||||
|
interval = seconds / 86400;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " days";
|
||||||
|
}
|
||||||
|
interval = seconds / 3600;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " hours";
|
||||||
|
}
|
||||||
|
interval = seconds / 60;
|
||||||
|
if (interval > 1) {
|
||||||
|
return Math.floor(interval) + " minutes";
|
||||||
|
}
|
||||||
|
return Math.floor(seconds) + " seconds";
|
||||||
|
}
|
||||||
|
|
||||||
let fn = async function(event) {
|
let fn = async function(event) {
|
||||||
let data = JSON.parse(event.data)
|
let data = JSON.parse(event.data)
|
||||||
const toast = document.createElement("div")
|
const toast = document.createElement("div")
|
||||||
|
|
|
@ -73,10 +73,10 @@
|
||||||
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2},
|
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2},
|
||||||
"blog.post": {"ops": ("fetch", "get"), "timeout": 20 * 15},
|
"blog.post": {"ops": ("fetch", "get"), "timeout": 20 * 15},
|
||||||
"themes.theme": {"ops": ("fetch", "get"), "timeout": 60 * 60},
|
"themes.theme": {"ops": ("fetch", "get"), "timeout": 60 * 60},
|
||||||
"gallery.*": {"ops": "all", "timeout": 60 * 15},
|
"gallery.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
|
||||||
"files.*": {"ops": ("fetch", "get"), "timeout": 60},
|
"files.*": {"ops": ("fetch", "get"), "timeout": 60},
|
||||||
"auth.permission": {"ops": "all", "timeout": 60 * 15},
|
"auth.permission": {"ops": "all", "timeout": 60 * 15},
|
||||||
"music.*": {"ops": "all", "timeout": 60 * 15},
|
"music.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
|
||||||
}
|
}
|
||||||
CACHEOPS_REDIS = env.str("REDIS_URL")
|
CACHEOPS_REDIS = env.str("REDIS_URL")
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user