mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 00:26:36 +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 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(Album)
|
||||
admin.site.register(Song)
|
||||
admin.site.register(Playlist)
|
||||
admin.site.register(PlaylistSong)
|
||||
admin.site.register(SongUserRating)
|
||||
|
|
|
@ -32,7 +32,7 @@ class SongSerializer(serializers.ModelSerializer):
|
|||
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
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:
|
||||
return SongUserRating.objects.filter(
|
||||
song=obj, user=self.context["request"].user, like=True
|
||||
|
@ -61,8 +61,8 @@ class Meta:
|
|||
|
||||
|
||||
class ListSongSerializer(SetUserModelSerializer):
|
||||
album = AlbumSerializer(read_only=True)
|
||||
authors = AuthorSerializer(many=True, read_only=True)
|
||||
album = serializers.SerializerMethodField(method_name="get_album")
|
||||
authors = serializers.SerializerMethodField(method_name="get_authors")
|
||||
liked = serializers.SerializerMethodField(method_name="get_liked")
|
||||
|
||||
@extend_schema_field(serializers.BooleanField)
|
||||
|
@ -73,6 +73,20 @@ def get_liked(self, obj):
|
|||
return obj.id in self.context["likes_ids"]
|
||||
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:
|
||||
model = Song
|
||||
fields = [
|
||||
|
|
|
@ -9,8 +9,11 @@
|
|||
ListCreatePlaylistAPIView,
|
||||
ListCreateSongAPIView,
|
||||
ListDislikedSongsAPIView,
|
||||
ListenSongAPIView,
|
||||
ListLikedSongsAPIView,
|
||||
ListPublicPlaylistAPIView,
|
||||
ListSongPlaylistsAPIView,
|
||||
ListUserListenedSongsAPIView,
|
||||
RemoveSongFromPlaylistAPIView,
|
||||
RetrieveUpdateDestroyAlbumAPIView,
|
||||
RetrieveUpdateDestroyAuthorAPIView,
|
||||
|
@ -42,10 +45,25 @@
|
|||
RetrieveUpdateDestroySongAPIView.as_view(),
|
||||
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(
|
||||
"song/<str:slug>/playlists/",
|
||||
ListSongPlaylistsAPIView.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/<str:slug>",
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from drf_spectacular.utils import OpenApiExample, OpenApiParameter, extend_schema
|
||||
from rest_framework import generics, permissions
|
||||
from rest_framework.response import Response
|
||||
|
||||
from akarpov.common.api.pagination import StandardResultsSetPagination
|
||||
from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly
|
||||
|
@ -15,7 +17,15 @@
|
|||
PlaylistSerializer,
|
||||
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):
|
||||
|
@ -73,19 +83,76 @@ class ListCreateSongAPIView(LikedSongsContextMixin, generics.ListCreateAPIView):
|
|||
pagination_class = StandardResultsSetPagination
|
||||
|
||||
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:
|
||||
return (
|
||||
Song.objects.all()
|
||||
.exclude(
|
||||
id__in=SongUserRating.objects.filter(
|
||||
user=self.request.user,
|
||||
like=False,
|
||||
).values_list("song_id", flat=True)
|
||||
)
|
||||
.prefetch_related("authors")
|
||||
.select_related("album")
|
||||
return qs.exclude(
|
||||
id__in=SongUserRating.objects.filter(
|
||||
user=self.request.user,
|
||||
like=False,
|
||||
).values_list("song_id", flat=True)
|
||||
)
|
||||
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):
|
||||
|
@ -142,15 +209,10 @@ def get_serializer_context(self):
|
|||
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")
|
||||
return Song.objects.cache().filter(
|
||||
id__in=SongUserRating.objects.cache()
|
||||
.filter(user=self.request.user, like=True)
|
||||
.values_list("song_id", flat=False)
|
||||
)
|
||||
|
||||
|
||||
|
@ -240,3 +302,45 @@ class RetrieveUpdateDestroyAuthorAPIView(
|
|||
lookup_url_kwarg = "slug"
|
||||
permission_classes = [IsAdminOrReadOnly]
|
||||
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:
|
||||
unique_together = ["song", "user"]
|
||||
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 celery import shared_task
|
||||
from channels.layers import get_channel_layer
|
||||
from django.utils.timezone import now
|
||||
from pytube import Channel, Playlist
|
||||
|
||||
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.file import load_dir, load_file
|
||||
from akarpov.utils.celery import get_scheduled_tasks_name
|
||||
|
@ -86,3 +87,29 @@ def start_next_song(previous_ids: list):
|
|||
countdown=song.length,
|
||||
)
|
||||
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')
|
||||
|
||||
|
||||
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 data = JSON.parse(event.data)
|
||||
const toast = document.createElement("div")
|
||||
|
|
|
@ -73,10 +73,10 @@
|
|||
"auth.*": {"ops": ("fetch", "get"), "timeout": 60 * 2},
|
||||
"blog.post": {"ops": ("fetch", "get"), "timeout": 20 * 15},
|
||||
"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},
|
||||
"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")
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user