Compare commits

..

2 Commits

Author SHA1 Message Date
dependabot[bot]
7b2b413966
Merge 7ad2ab043a into fdfe839d19 2023-12-11 14:33:52 +00:00
dependabot[bot]
7ad2ab043a
Bump rawpy from 0.18.1 to 0.19.0
Bumps [rawpy](https://github.com/letmaik/rawpy) from 0.18.1 to 0.19.0.
- [Release notes](https://github.com/letmaik/rawpy/releases)
- [Commits](https://github.com/letmaik/rawpy/compare/v0.18.1...v0.19.0)

---
updated-dependencies:
- dependency-name: rawpy
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-12-11 14:33:50 +00:00
13 changed files with 45 additions and 389 deletions

View File

@ -1,17 +1,9 @@
from django.contrib import admin
from akarpov.music.models import (
Album,
Author,
Playlist,
PlaylistSong,
Song,
SongUserRating,
)
from akarpov.music.models import Album, Author, Playlist, PlaylistSong, Song
admin.site.register(Author)
admin.site.register(Album)
admin.site.register(Song)
admin.site.register(Playlist)
admin.site.register(PlaylistSong)
admin.site.register(SongUserRating)

View File

@ -32,7 +32,7 @@ class SongSerializer(serializers.ModelSerializer):
@extend_schema_field(serializers.BooleanField)
def get_liked(self, obj):
if "request" in self.context and self.context["request"]:
if "request" in self.context:
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 = serializers.SerializerMethodField(method_name="get_album")
authors = serializers.SerializerMethodField(method_name="get_authors")
album = AlbumSerializer(read_only=True)
authors = AuthorSerializer(many=True, read_only=True)
liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
@ -73,20 +73,6 @@ 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 = [
@ -239,26 +225,10 @@ class Meta:
class FullAlbumSerializer(serializers.ModelSerializer):
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:
model = Album
fields = ["name", "link", "image", "songs", "artists"]
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
@ -267,27 +237,10 @@ class Meta:
class FullAuthorSerializer(serializers.ModelSerializer):
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:
model = Author
fields = ["name", "link", "image", "songs", "albums"]
fields = ["name", "link", "image", "songs"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},

View File

@ -9,11 +9,8 @@
ListCreatePlaylistAPIView,
ListCreateSongAPIView,
ListDislikedSongsAPIView,
ListenSongAPIView,
ListLikedSongsAPIView,
ListPublicPlaylistAPIView,
ListSongPlaylistsAPIView,
ListUserListenedSongsAPIView,
RemoveSongFromPlaylistAPIView,
RetrieveUpdateDestroyAlbumAPIView,
RetrieveUpdateDestroyAuthorAPIView,
@ -45,25 +42,10 @@
RetrieveUpdateDestroySongAPIView.as_view(),
name="retrieve_update_delete_song",
),
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("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>",

View File

@ -1,6 +1,4 @@
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
@ -17,15 +15,7 @@
PlaylistSerializer,
SongSerializer,
)
from akarpov.music.models import (
Album,
Author,
Playlist,
Song,
SongUserRating,
UserListenHistory,
)
from akarpov.music.tasks import listen_to_song
from akarpov.music.models import Album, Author, Playlist, Song, SongUserRating
class LikedSongsContextMixin(generics.GenericAPIView):
@ -83,76 +73,19 @@ 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 qs.exclude(
id__in=SongUserRating.objects.filter(
user=self.request.user,
like=False,
).values_list("song_id", flat=True)
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
@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)
return Song.objects.all().prefetch_related("authors").select_related("album")
class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
@ -209,10 +142,15 @@ 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)
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")
)
@ -272,7 +210,9 @@ class ListAlbumsAPIView(generics.ListAPIView):
serializer_class = AlbumSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
queryset = Album.objects.cache().all()
def get_queryset(self):
return Album.objects.all()
class RetrieveUpdateDestroyAlbumAPIView(
@ -282,14 +222,15 @@ class RetrieveUpdateDestroyAlbumAPIView(
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
serializer_class = FullAlbumSerializer
queryset = Album.objects.cache().all()
class ListAuthorsAPIView(generics.ListAPIView):
serializer_class = AuthorSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
queryset = Author.objects.cache().all()
def get_queryset(self):
return Author.objects.all()
class RetrieveUpdateDestroyAuthorAPIView(
@ -299,46 +240,3 @@ class RetrieveUpdateDestroyAuthorAPIView(
lookup_url_kwarg = "slug"
permission_classes = [IsAdminOrReadOnly]
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)
)

View File

@ -1,54 +0,0 @@
# 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"],
},
),
]

View File

@ -147,14 +147,3 @@ 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"]

View File

@ -1,8 +1,6 @@
import os
from deep_translator import GoogleTranslator
from django.core.files import File
from django.utils.text import slugify
from mutagen import File as MutagenFile
from mutagen.id3 import APIC, ID3, TCON, TORY, TextFrame
from mutagen.mp3 import MP3
@ -74,32 +72,16 @@ def load_track(
if 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:
with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(image, name=image_path.split("/")[-1])
song.file = File(file, name=new_file_name)
song.file = File(file, name=path.split("/")[-1])
song.save()
else:
with open(path, "rb") as file:
song.file = File(file, name=new_file_name)
song.file = File(file, name=path.split("/")[-1])
song.save()
if not album.image and song.image:
album.image = song.image
album.save()
if authors:
song.authors.set(authors)

View File

@ -3,7 +3,7 @@
from django.db.models.signals import post_delete, post_save, pre_save
from django.dispatch import receiver
from akarpov.music.models import Album, Author, PlaylistSong, Song, SongUserRating
from akarpov.music.models import PlaylistSong, Song, SongUserRating
@receiver(post_delete, sender=Song)
@ -13,20 +13,6 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
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)
def send_que_status(sender, instance, created, **kwargs):
...

View File

@ -1,11 +1,10 @@
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, UserListenHistory
from akarpov.music.models import RadioSong, Song
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
@ -87,29 +86,3 @@ 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

View File

@ -159,35 +159,6 @@
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")

View File

@ -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": ("fetch", "get", "list"), "timeout": 60 * 15},
"files.*": {"ops": ("fetch", "get", "list"), "timeout": 60},
"gallery.*": {"ops": "all", "timeout": 60 * 15},
"files.*": {"ops": ("fetch", "get"), "timeout": 60},
"auth.permission": {"ops": "all", "timeout": 60 * 15},
"music.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
"music.*": {"ops": "all", "timeout": 60 * 15},
}
CACHEOPS_REDIS = env.str("REDIS_URL")

17
poetry.lock generated
View File

@ -1327,21 +1327,6 @@ files = [
{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]]
name = "defusedxml"
version = "0.7.1"
@ -6719,4 +6704,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "f3dd82fbbee6bd4accfca7000c9de3102b49478dd18365887a56590c2a9e84d7"
content-hash = "b3ff2e53b9e9ee3a3a3c5985e09987ca3114fb34869d0c76b771535d4d38f2ac"

View File

@ -114,7 +114,6 @@ pydantic-settings = "^2.0.3"
django-elasticsearch-dsl = "^8.0"
elasticsearch-dsl = "^8.11.0"
numpy = "1.25.2"
deep-translator = "1.4.2"
[build-system]