mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-28 04:33:43 +03:00
Compare commits
6 Commits
5cc4c5067d
...
dc5dc02472
Author | SHA1 | Date | |
---|---|---|---|
|
dc5dc02472 | ||
43a6e8d779 | |||
b16ec21486 | |||
18ba02caab | |||
7b48929e38 | |||
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 = [
|
||||||
|
@ -225,10 +239,26 @@ class Meta:
|
||||||
|
|
||||||
class FullAlbumSerializer(serializers.ModelSerializer):
|
class FullAlbumSerializer(serializers.ModelSerializer):
|
||||||
songs = ListSongSerializer(many=True, read_only=True)
|
songs = ListSongSerializer(many=True, read_only=True)
|
||||||
|
artists = serializers.SerializerMethodField("get_artists")
|
||||||
|
|
||||||
|
@extend_schema_field(AuthorSerializer(many=True))
|
||||||
|
def get_artists(self, obj):
|
||||||
|
artists = []
|
||||||
|
qs = Author.objects.cache().filter(
|
||||||
|
songs__id__in=obj.songs.cache().all().values("id").distinct()
|
||||||
|
)
|
||||||
|
for artist in qs:
|
||||||
|
if artist not in artists:
|
||||||
|
artists.append(artist)
|
||||||
|
|
||||||
|
return AuthorSerializer(
|
||||||
|
artists,
|
||||||
|
many=True,
|
||||||
|
).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Album
|
model = Album
|
||||||
fields = ["name", "link", "image", "songs"]
|
fields = ["name", "link", "image", "songs", "artists"]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"link": {"read_only": True},
|
"link": {"read_only": True},
|
||||||
"image": {"read_only": True},
|
"image": {"read_only": True},
|
||||||
|
@ -237,10 +267,27 @@ class Meta:
|
||||||
|
|
||||||
class FullAuthorSerializer(serializers.ModelSerializer):
|
class FullAuthorSerializer(serializers.ModelSerializer):
|
||||||
songs = ListSongSerializer(many=True, read_only=True)
|
songs = ListSongSerializer(many=True, read_only=True)
|
||||||
|
albums = serializers.SerializerMethodField(method_name="get_albums")
|
||||||
|
|
||||||
|
@extend_schema_field(AlbumSerializer(many=True))
|
||||||
|
def get_albums(self, obj):
|
||||||
|
qs = Album.objects.cache().filter(
|
||||||
|
songs__id__in=obj.songs.cache().all().values("id").distinct()
|
||||||
|
)
|
||||||
|
albums = []
|
||||||
|
for album in qs:
|
||||||
|
# TODO: rewrite to filter
|
||||||
|
if album not in albums:
|
||||||
|
albums.append(album)
|
||||||
|
|
||||||
|
return AlbumSerializer(
|
||||||
|
albums,
|
||||||
|
many=True,
|
||||||
|
).data
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Author
|
model = Author
|
||||||
fields = ["name", "link", "image", "songs"]
|
fields = ["name", "link", "image", "songs", "albums"]
|
||||||
extra_kwargs = {
|
extra_kwargs = {
|
||||||
"link": {"read_only": True},
|
"link": {"read_only": True},
|
||||||
"image": {"read_only": True},
|
"image": {"read_only": True},
|
||||||
|
|
|
@ -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")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -210,9 +272,7 @@ class ListAlbumsAPIView(generics.ListAPIView):
|
||||||
serializer_class = AlbumSerializer
|
serializer_class = AlbumSerializer
|
||||||
pagination_class = StandardResultsSetPagination
|
pagination_class = StandardResultsSetPagination
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
queryset = Album.objects.cache().all()
|
||||||
def get_queryset(self):
|
|
||||||
return Album.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveUpdateDestroyAlbumAPIView(
|
class RetrieveUpdateDestroyAlbumAPIView(
|
||||||
|
@ -222,15 +282,14 @@ class RetrieveUpdateDestroyAlbumAPIView(
|
||||||
lookup_url_kwarg = "slug"
|
lookup_url_kwarg = "slug"
|
||||||
permission_classes = [IsAdminOrReadOnly]
|
permission_classes = [IsAdminOrReadOnly]
|
||||||
serializer_class = FullAlbumSerializer
|
serializer_class = FullAlbumSerializer
|
||||||
|
queryset = Album.objects.cache().all()
|
||||||
|
|
||||||
|
|
||||||
class ListAuthorsAPIView(generics.ListAPIView):
|
class ListAuthorsAPIView(generics.ListAPIView):
|
||||||
serializer_class = AuthorSerializer
|
serializer_class = AuthorSerializer
|
||||||
pagination_class = StandardResultsSetPagination
|
pagination_class = StandardResultsSetPagination
|
||||||
permission_classes = [permissions.AllowAny]
|
permission_classes = [permissions.AllowAny]
|
||||||
|
queryset = Author.objects.cache().all()
|
||||||
def get_queryset(self):
|
|
||||||
return Author.objects.all()
|
|
||||||
|
|
||||||
|
|
||||||
class RetrieveUpdateDestroyAuthorAPIView(
|
class RetrieveUpdateDestroyAuthorAPIView(
|
||||||
|
@ -240,3 +299,46 @@ class RetrieveUpdateDestroyAuthorAPIView(
|
||||||
lookup_url_kwarg = "slug"
|
lookup_url_kwarg = "slug"
|
||||||
permission_classes = [IsAdminOrReadOnly]
|
permission_classes = [IsAdminOrReadOnly]
|
||||||
serializer_class = FullAuthorSerializer
|
serializer_class = FullAuthorSerializer
|
||||||
|
queryset = Author.objects.cache().all()
|
||||||
|
|
||||||
|
|
||||||
|
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,6 +1,8 @@
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
from deep_translator import GoogleTranslator
|
||||||
from django.core.files import File
|
from django.core.files import File
|
||||||
|
from django.utils.text import slugify
|
||||||
from mutagen import File as MutagenFile
|
from mutagen import File as MutagenFile
|
||||||
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
|
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
|
||||||
from mutagen.mp3 import MP3
|
from mutagen.mp3 import MP3
|
||||||
|
@ -72,16 +74,32 @@ def load_track(
|
||||||
if kwargs:
|
if kwargs:
|
||||||
song.meta = kwargs
|
song.meta = kwargs
|
||||||
|
|
||||||
|
new_file_name = (
|
||||||
|
str(
|
||||||
|
slugify(
|
||||||
|
GoogleTranslator(source="auto", target="en").translate(
|
||||||
|
f"{song.name} {' '.join([x.name for x in song.authors])}",
|
||||||
|
target_language="en",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
+ ".mp3"
|
||||||
|
)
|
||||||
|
|
||||||
if image_path:
|
if image_path:
|
||||||
with open(path, "rb") as file, open(image_path, "rb") as image:
|
with open(path, "rb") as file, open(image_path, "rb") as image:
|
||||||
song.image = File(image, name=image_path.split("/")[-1])
|
song.image = File(image, name=image_path.split("/")[-1])
|
||||||
song.file = File(file, name=path.split("/")[-1])
|
song.file = File(file, name=new_file_name)
|
||||||
song.save()
|
song.save()
|
||||||
else:
|
else:
|
||||||
with open(path, "rb") as file:
|
with open(path, "rb") as file:
|
||||||
song.file = File(file, name=path.split("/")[-1])
|
song.file = File(file, name=new_file_name)
|
||||||
song.save()
|
song.save()
|
||||||
|
|
||||||
|
if not album.image and song.image:
|
||||||
|
album.image = song.image
|
||||||
|
album.save()
|
||||||
|
|
||||||
if authors:
|
if authors:
|
||||||
song.authors.set(authors)
|
song.authors.set(authors)
|
||||||
|
|
||||||
|
|
|
@ -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 PlaylistSong, Song, SongUserRating
|
from akarpov.music.models import Album, Author, PlaylistSong, Song, SongUserRating
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_delete, sender=Song)
|
@receiver(post_delete, sender=Song)
|
||||||
|
@ -13,6 +13,20 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
|
||||||
os.remove(instance.file.path)
|
os.remove(instance.file.path)
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Author)
|
||||||
|
def author_create(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
# TODO: add logic to retrieve author info here
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(post_save, sender=Album)
|
||||||
|
def album_create(sender, instance, created, **kwargs):
|
||||||
|
if created:
|
||||||
|
# TODO: add logic to retrieve author info here
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
@receiver(post_save)
|
@receiver(post_save)
|
||||||
def send_que_status(sender, instance, created, **kwargs):
|
def send_que_status(sender, instance, created, **kwargs):
|
||||||
...
|
...
|
||||||
|
|
|
@ -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", "list"), "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")
|
||||||
|
|
||||||
|
|
37
poetry.lock
generated
37
poetry.lock
generated
|
@ -1327,6 +1327,21 @@ files = [
|
||||||
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deep-translator"
|
||||||
|
version = "1.4.2"
|
||||||
|
description = "A flexible python tool to translate between different languages in a simple way."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.0"
|
||||||
|
files = [
|
||||||
|
{file = "deep_translator-1.4.2-py2.py3-none-any.whl", hash = "sha256:85ec4cf52b9a48bdabc992eb931545abd2a0c63e092e50773d14f7bc506e6e89"},
|
||||||
|
{file = "deep_translator-1.4.2.tar.gz", hash = "sha256:1af1feaaa351904e2db10d5fa86a09009839ad2252c0981150350d962d6b3dc4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
beautifulsoup4 = "*"
|
||||||
|
requests = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "defusedxml"
|
name = "defusedxml"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
@ -1950,17 +1965,6 @@ compatible-mypy = ["mypy (>=0.991,<0.1000)"]
|
||||||
coreapi = ["coreapi (>=2.0.0)"]
|
coreapi = ["coreapi (>=2.0.0)"]
|
||||||
markdown = ["types-Markdown (>=0.1.5)"]
|
markdown = ["types-Markdown (>=0.1.5)"]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "docopt-ng"
|
|
||||||
version = "0.9.0"
|
|
||||||
description = "Jazzband-maintained fork of docopt, the humane command line arguments parser."
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
files = [
|
|
||||||
{file = "docopt_ng-0.9.0-py3-none-any.whl", hash = "sha256:bfe4c8b03f9fca424c24ee0b4ffa84bf7391cb18c29ce0f6a8227a3b01b81ff9"},
|
|
||||||
{file = "docopt_ng-0.9.0.tar.gz", hash = "sha256:91c6da10b5bb6f2e9e25345829fb8278c78af019f6fc40887ad49b060483b1d7"},
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "docutils"
|
name = "docutils"
|
||||||
version = "0.20.1"
|
version = "0.20.1"
|
||||||
|
@ -4518,21 +4522,22 @@ pylint = ">=1.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pymorphy3"
|
name = "pymorphy3"
|
||||||
version = "1.2.1"
|
version = "1.3.1"
|
||||||
description = "Morphological analyzer (POS tagger + inflection engine) for Russian language."
|
description = "Morphological analyzer (POS tagger + inflection engine) for Russian language."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
files = [
|
files = [
|
||||||
{file = "pymorphy3-1.2.1-py3-none-any.whl", hash = "sha256:88700966f55e77e3d2aedf194fa00bb4a175c2626017fe423e94ce11bc98f1ff"},
|
{file = "pymorphy3-1.3.1-py3-none-any.whl", hash = "sha256:bc495d245e6155b1ce77b8aa4d5a7d7e27c678d564338401f8f99271812c40c2"},
|
||||||
{file = "pymorphy3-1.2.1.tar.gz", hash = "sha256:0cc186a3b0716129dd45e3b89f5e8339e5943d9013f93cfd4c58e5335daf296d"},
|
{file = "pymorphy3-1.3.1.tar.gz", hash = "sha256:49b5651aa76dadabe0fb2e6a1ef73619c0b434ed118916ce0211dd7a4cb60cd8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
dawg-python = ">=0.7.1"
|
dawg-python = ">=0.7.1"
|
||||||
docopt-ng = ">=0.6"
|
|
||||||
pymorphy3-dicts-ru = "*"
|
pymorphy3-dicts-ru = "*"
|
||||||
|
setuptools = {version = ">=68.2.2", markers = "python_version >= \"3.12\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
|
cli = ["click"]
|
||||||
fast = ["DAWG (>=0.8)"]
|
fast = ["DAWG (>=0.8)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -6704,4 +6709,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.11"
|
||||||
content-hash = "7b52426065b04709a15a560787f01a00166dba6005aea755ed490a7523569524"
|
content-hash = "4f36b172cecf2647893a5d7ac50bc14489b331b9a5bd11811094b9270842f6b0"
|
||||||
|
|
|
@ -107,13 +107,14 @@ textract = "^1.6.5"
|
||||||
uuid6 = "^2023.5.2"
|
uuid6 = "^2023.5.2"
|
||||||
uvicorn = "^0.24.0.post1"
|
uvicorn = "^0.24.0.post1"
|
||||||
nltk = "^3.8.1"
|
nltk = "^3.8.1"
|
||||||
pymorphy3 = "^1.2.1"
|
pymorphy3 = "^1.3.1"
|
||||||
pymorphy3-dicts-ru = "^2.4.417150.4580142"
|
pymorphy3-dicts-ru = "^2.4.417150.4580142"
|
||||||
fastapi = "^0.104.1"
|
fastapi = "^0.104.1"
|
||||||
pydantic-settings = "^2.0.3"
|
pydantic-settings = "^2.0.3"
|
||||||
django-elasticsearch-dsl = "^8.0"
|
django-elasticsearch-dsl = "^8.0"
|
||||||
elasticsearch-dsl = "^8.11.0"
|
elasticsearch-dsl = "^8.11.0"
|
||||||
numpy = "1.25.2"
|
numpy = "1.25.2"
|
||||||
|
deep-translator = "1.4.2"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|
Loading…
Reference in New Issue
Block a user