mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-24 13:33:44 +03:00
Compare commits
7 Commits
7b2b413966
...
b45ffbf915
Author | SHA1 | Date | |
---|---|---|---|
|
b45ffbf915 | ||
|
d4aaa05eb3 | ||
43a6e8d779 | |||
b16ec21486 | |||
18ba02caab | |||
7b48929e38 | |||
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 = [
|
||||
|
@ -225,10 +239,26 @@ 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"]
|
||||
fields = ["name", "link", "image", "songs", "artists"]
|
||||
extra_kwargs = {
|
||||
"link": {"read_only": True},
|
||||
"image": {"read_only": True},
|
||||
|
@ -237,10 +267,27 @@ 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"]
|
||||
fields = ["name", "link", "image", "songs", "albums"]
|
||||
extra_kwargs = {
|
||||
"link": {"read_only": True},
|
||||
"image": {"read_only": True},
|
||||
|
|
|
@ -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(
|
||||
return qs.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",
|
||||
),
|
||||
],
|
||||
),
|
||||
]
|
||||
)
|
||||
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):
|
||||
|
@ -142,16 +209,11 @@ def get_serializer_context(self):
|
|||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
Song.objects.cache()
|
||||
.filter(
|
||||
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")
|
||||
)
|
||||
|
||||
|
||||
class AddSongToPlaylistAPIView(generics.CreateAPIView):
|
||||
|
@ -210,9 +272,7 @@ class ListAlbumsAPIView(generics.ListAPIView):
|
|||
serializer_class = AlbumSerializer
|
||||
pagination_class = StandardResultsSetPagination
|
||||
permission_classes = [permissions.AllowAny]
|
||||
|
||||
def get_queryset(self):
|
||||
return Album.objects.all()
|
||||
queryset = Album.objects.cache().all()
|
||||
|
||||
|
||||
class RetrieveUpdateDestroyAlbumAPIView(
|
||||
|
@ -222,15 +282,14 @@ 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]
|
||||
|
||||
def get_queryset(self):
|
||||
return Author.objects.all()
|
||||
queryset = Author.objects.cache().all()
|
||||
|
||||
|
||||
class RetrieveUpdateDestroyAuthorAPIView(
|
||||
|
@ -240,3 +299,46 @@ 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)
|
||||
)
|
||||
|
|
|
@ -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,6 +1,8 @@
|
|||
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
|
||||
|
@ -72,16 +74,32 @@ 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=path.split("/")[-1])
|
||||
song.file = File(file, name=new_file_name)
|
||||
song.save()
|
||||
else:
|
||||
with open(path, "rb") as file:
|
||||
song.file = File(file, name=path.split("/")[-1])
|
||||
song.file = File(file, name=new_file_name)
|
||||
song.save()
|
||||
|
||||
if not album.image and song.image:
|
||||
album.image = song.image
|
||||
album.save()
|
||||
|
||||
if authors:
|
||||
song.authors.set(authors)
|
||||
|
||||
|
|
|
@ -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 PlaylistSong, Song, SongUserRating
|
||||
from akarpov.music.models import Album, Author, PlaylistSong, Song, SongUserRating
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Song)
|
||||
|
@ -13,6 +13,20 @@ 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):
|
||||
...
|
||||
|
|
|
@ -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},
|
||||
"files.*": {"ops": ("fetch", "get"), "timeout": 60},
|
||||
"gallery.*": {"ops": ("fetch", "get", "list"), "timeout": 60 * 15},
|
||||
"files.*": {"ops": ("fetch", "get", "list"), "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")
|
||||
|
||||
|
|
17
poetry.lock
generated
17
poetry.lock
generated
|
@ -1327,6 +1327,21 @@ 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"
|
||||
|
@ -6704,4 +6719,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "b3ff2e53b9e9ee3a3a3c5985e09987ca3114fb34869d0c76b771535d4d38f2ac"
|
||||
content-hash = "f3dd82fbbee6bd4accfca7000c9de3102b49478dd18365887a56590c2a9e84d7"
|
||||
|
|
|
@ -114,6 +114,7 @@ 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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user