Compare commits

...

7 Commits

Author SHA1 Message Date
dependabot[bot]
b45ffbf915
Merge d4aaa05eb3 into 43a6e8d779 2023-12-17 21:44:46 +00:00
dependabot[bot]
d4aaa05eb3
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-17 21:44:43 +00:00
43a6e8d779 updated music processing 2023-12-18 00:35:58 +03:00
b16ec21486 fixed music artist and albums relations 2023-12-17 16:36:30 +03:00
18ba02caab fixed files 2023-12-17 16:22:40 +03:00
7b48929e38 fixed albums and authors, added related objects 2023-12-17 16:19:00 +03:00
127b4b6e11 major music updates, minor template fixes 2023-12-16 21:11:08 +03:00
13 changed files with 411 additions and 67 deletions

View File

@ -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)

View File

@ -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},

View File

@ -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>",

View File

@ -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()
.exclude(
id__in=SongUserRating.objects.filter( id__in=SongUserRating.objects.filter(
user=self.request.user, user=self.request.user,
like=False, like=False,
).values_list("song_id", flat=True) ).values_list("song_id", flat=True)
) )
.prefetch_related("authors") return qs
.select_related("album")
@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",
),
],
),
]
) )
return Song.objects.all().prefetch_related("authors").select_related("album") def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView): class RetrieveUpdateDestroySongAPIView(generics.RetrieveUpdateDestroyAPIView):
@ -142,16 +209,11 @@ 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()
.filter(
id__in=SongUserRating.objects.cache() id__in=SongUserRating.objects.cache()
.filter(user=self.request.user, like=True) .filter(user=self.request.user, like=True)
.values_list("song_id", flat=False) .values_list("song_id", flat=False)
) )
.prefetch_related("authors")
.select_related("album")
)
class AddSongToPlaylistAPIView(generics.CreateAPIView): class AddSongToPlaylistAPIView(generics.CreateAPIView):
@ -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)
)

View File

@ -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"],
},
),
]

View File

@ -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"]

View File

@ -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)

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 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):
... ...

View File

@ -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

View File

@ -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")

View File

@ -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")

59
poetry.lock generated
View File

@ -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"
@ -4941,31 +4956,31 @@ files = [
[[package]] [[package]]
name = "rawpy" name = "rawpy"
version = "0.18.1" version = "0.19.0"
description = "RAW image processing for Python, a wrapper for libraw" description = "RAW image processing for Python, a wrapper for libraw"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "rawpy-0.18.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:30f5a7f3b80db8aa18876831b73b5cf0530da3f2dd8be9870cc1bae271b5fdb0"}, {file = "rawpy-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:663766ad644e771491c31b597671cf4780da8449289492fd4cfc3689aefe47dc"},
{file = "rawpy-0.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6cdb86817f4740c3252d4797e0b5926d499330b46da1255d56d6a401a34c3ec"}, {file = "rawpy-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c094ca1a0b892e6b9fb8f8bf177a92d94af4218dee8ee6496488a309b7015dd6"},
{file = "rawpy-0.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0a040c33fe3b14a6bd9d81a8c2d6cb49657530ede982ca1585108229fec651"}, {file = "rawpy-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5eee1379200325171f976518918930357722d21b4241476c8537ac6884ed3f16"},
{file = "rawpy-0.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:bc2fc6e346278ea3fc0fe76b704984551055914905e6187be7844fecfc756b22"}, {file = "rawpy-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:ece54693d92416d91b149e6ee50f479dd0be81858ac78e7c5e1047e19115c328"},
{file = "rawpy-0.18.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e836ff1a0f7d8eb24665f4f230a41f14eda66a86445b372b906c2cf8651a5d17"}, {file = "rawpy-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e575d5bfa0168bbded845bde921dcde03bb7ffc66190a902fbf6b8a7473e87a1"},
{file = "rawpy-0.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3b352da1316f5ac9a41e418f6247587420ff9966441b95f2aa6cefdb167c51"}, {file = "rawpy-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fa3e5b275c49f77d4ba9d177851e5d499ad34389b509b9f64b4fec2ffcece337"},
{file = "rawpy-0.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fac80b3aa1cec375345c14a63c1c0360520ef34b7f1150478cf453e43585c1c"}, {file = "rawpy-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f6bfc5b9126de96b1f67cda7d39bbb28a9af1ed8b91c5a1fd9417f4d6327749"},
{file = "rawpy-0.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:fff49c7529f3c06aff2daa9d61a092a0f13ca30fdf722b6d12ca5cff9e21180a"}, {file = "rawpy-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:80cb145a8710fdeded60a79bcdf8224c8349c26e7addb7253604dbb36e3bc0b0"},
{file = "rawpy-0.18.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:296bb1bcc397de4a9e018cb4136c395f7c49547eae9e3de1b5e785db2b23657a"}, {file = "rawpy-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:064e657c478b330a4460c5b58f5565dea6b2003013e036ca3058386bc3521fe1"},
{file = "rawpy-0.18.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08160b50b4b63a9150334c79a30c2c24c69824386c3a92fa2d8c66a2a29680f6"}, {file = "rawpy-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe6b7fc7d1d53ed06962a46aa723b09d593bbf70d23c5cb4b1a6a3fb64276cd6"},
{file = "rawpy-0.18.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6effc4a49f64acaa01e35ec42b7c29f2e7453b8f010dbcf4aacd0f1394c186c"}, {file = "rawpy-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0e4e49ebdbb6d4d1f6ec7b93406b4da53372123f0ee13ec1682bdcf20b238f"},
{file = "rawpy-0.18.1-cp37-cp37m-win_amd64.whl", hash = "sha256:a4f4f51d55e073394340cb52f5fcb4bb2d1c596884666ee85e61c15b9c2eef59"}, {file = "rawpy-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:285ef8f89b59826344513c01d66e7e1e7d07e968e21a41908eec5593368fc88a"},
{file = "rawpy-0.18.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:710fdaf4af31171d847eae4dc4bbd717a951d75d159cdcc27a9ee8b046354447"}, {file = "rawpy-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:696612059fab1c1f5b62605aa217248bfd1639906f665f6b9e7e17b78f67d119"},
{file = "rawpy-0.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18171458cff40f968797fac4681aed6ec9bf3b2278b2235d39b09f987d2470b8"}, {file = "rawpy-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0c6a829ef472f0c8684ed2d650aedab4c8a8713da54bfa94e479a5e483b3ae4"},
{file = "rawpy-0.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfd92c94eae2f8bdde4b249007f8207e74f0bc8f3f5998e6784eaf1e9b67dd7a"}, {file = "rawpy-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21bb29104425fbaeb7269acf33ca5dd286f7fc913515c4abe4c4ff004ed860be"},
{file = "rawpy-0.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:d37c144ac4922ce20acccf93bb97b2d5a3e06ab782de58d9630bd77733962cb6"}, {file = "rawpy-0.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:e53b6f2a85fa6c2bea64ea0c82242b6d62b725f837d30825869f877ffbe3323c"},
{file = "rawpy-0.18.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:be306f686039dc2e2f7374ba15ce64b4392f10ca586f6ca6dd3252777588e750"}, {file = "rawpy-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd562fc1882405eca01515642caab773dfec5e54cd4c10fe3f4ef39d8526aea8"},
{file = "rawpy-0.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10cea749fc9d8cf1ae716172dd09b36accfd1de576351ce6a650b5b30f9dc6f8"}, {file = "rawpy-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9da7fec99d2403f01ab3dc13b510313cb42eb76e0f1307d861bdf165fd1c467"},
{file = "rawpy-0.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1a6437ebdac9c3ce09932d752eac7acfda6e56a7996b211ac2a625b80168dad"}, {file = "rawpy-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4120e8f22dd147abba331fc14f51ea20b62758dee46b76f893c59afeb636c7cd"},
{file = "rawpy-0.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:139715d16ff64c47f53972e6b07576ce5c47cef899a187b149750855d08ef557"}, {file = "rawpy-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:5ca0ecf879de387f5c47d8d7308a0afaa259e63ff18c032637255879083a0102"},
] ]
[package.dependencies] [package.dependencies]
@ -6704,4 +6719,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 = "f3dd82fbbee6bd4accfca7000c9de3102b49478dd18365887a56590c2a9e84d7"

View File

@ -77,7 +77,7 @@ pydub = "^0.25.1"
python-mpd2 = "^3.0.5" python-mpd2 = "^3.0.5"
yandex-music = "^2.1.1" yandex-music = "^2.1.1"
pyjwt = "^2.8.0" pyjwt = "^2.8.0"
rawpy = "^0.18.1" rawpy = "^0.19.0"
xvfbwrapper = "^0.2.9" xvfbwrapper = "^0.2.9"
vtk = "^9.2.6" vtk = "^9.2.6"
ffmpeg-python = "^0.2.0" ffmpeg-python = "^0.2.0"
@ -114,6 +114,7 @@ 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]