Add music information service and refactor music serializers

This commit is contained in:
Alexander Karpov 2024-01-11 22:08:11 +03:00
parent 7cb24369ea
commit 0a49519a4e
12 changed files with 1111 additions and 199 deletions

View File

@ -1,17 +1,149 @@
from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from akarpov.music.models import (
from .models import (
Album,
Author,
Playlist,
PlaylistSong,
RadioSong,
Song,
SongInQue,
SongUserRating,
TempFileUpload,
UserListenHistory,
)
admin.site.register(Author)
admin.site.register(Album)
admin.site.register(Song)
admin.site.register(Playlist)
admin.site.register(PlaylistSong)
admin.site.register(SongUserRating)
class JSONFieldAdmin(admin.ModelAdmin):
def render_meta_field(self, obj):
meta = obj.meta
if not meta:
return "No data"
html_content = (
"<div style='max-height: 200px; overflow-y: scroll;'><pre>{}</pre></div>"
)
return format_html(html_content, mark_safe(meta))
readonly_fields = ("render_meta_field",)
class PlaylistSongInline(admin.TabularInline):
model = PlaylistSong
extra = 1
class SongUserRatingInline(admin.TabularInline):
model = SongUserRating
extra = 1
class UserListenHistoryInline(admin.TabularInline):
model = UserListenHistory
extra = 1
class SongInQueInline(admin.TabularInline):
model = SongInQue
extra = 1
class SongInline(admin.TabularInline):
model = Song
extra = 1
class AuthorAdmin(JSONFieldAdmin):
list_display = ("name", "slug")
search_fields = ["name"]
def render_meta_field(self, obj):
meta = super().render_meta_field(obj)
return meta
admin.site.register(Author, AuthorAdmin)
class AlbumAdmin(JSONFieldAdmin):
list_display = ("name", "link")
search_fields = ["name"]
inlines = [SongInline]
def render_meta_field(self, obj):
meta = super().render_meta_field(obj)
return meta
admin.site.register(Album, AlbumAdmin)
class SongAdmin(JSONFieldAdmin):
list_display = ("name", "link", "length", "played")
search_fields = ["name", "authors__name", "album__name"]
inlines = [PlaylistSongInline, SongUserRatingInline, UserListenHistoryInline]
def render_meta_field(self, obj):
meta = super().render_meta_field(obj)
return meta
admin.site.register(Song, SongAdmin)
class PlaylistAdmin(admin.ModelAdmin):
list_display = ("name", "private", "creator", "length")
search_fields = ["name", "creator__username"]
inlines = [PlaylistSongInline]
admin.site.register(Playlist, PlaylistAdmin)
class PlaylistSongAdmin(admin.ModelAdmin):
list_display = ("playlist", "song", "order")
search_fields = ["playlist__name", "song__name"]
admin.site.register(PlaylistSong, PlaylistSongAdmin)
class SongInQueAdmin(admin.ModelAdmin):
list_display = ("name", "status", "error")
search_fields = ["name"]
admin.site.register(SongInQue, SongInQueAdmin)
class TempFileUploadAdmin(admin.ModelAdmin):
list_display = ("file",)
search_fields = ["file"]
admin.site.register(TempFileUpload, TempFileUploadAdmin)
class RadioSongAdmin(admin.ModelAdmin):
list_display = ("start", "slug", "song")
search_fields = ["song__name", "slug"]
admin.site.register(RadioSong, RadioSongAdmin)
class SongUserRatingAdmin(admin.ModelAdmin):
list_display = ("song", "user", "like", "created")
search_fields = ["song__name", "user__username"]
admin.site.register(SongUserRating, SongUserRatingAdmin)
class UserListenHistoryAdmin(admin.ModelAdmin):
list_display = ("user", "song", "created")
search_fields = ["user__username", "song__name"]
admin.site.register(UserListenHistory, UserListenHistoryAdmin)

View File

@ -13,21 +13,21 @@
from akarpov.users.api.serializers import UserPublicInfoSerializer
class AuthorSerializer(serializers.ModelSerializer):
class ListAuthorSerializer(serializers.ModelSerializer):
class Meta:
model = Author
fields = ["name", "slug", "image_cropped"]
class AlbumSerializer(serializers.ModelSerializer):
class ListAlbumSerializer(serializers.ModelSerializer):
class Meta:
model = Album
fields = ["name", "slug", "image_cropped"]
class SongSerializer(serializers.ModelSerializer):
authors = AuthorSerializer(many=True)
album = AlbumSerializer()
authors = ListAuthorSerializer(many=True)
album = ListAlbumSerializer()
liked = serializers.SerializerMethodField(method_name="get_liked")
@extend_schema_field(serializers.BooleanField)
@ -51,6 +51,7 @@ class Meta:
"authors",
"album",
"liked",
"meta",
]
extra_kwargs = {
"slug": {"read_only": True},
@ -73,16 +74,16 @@ def get_liked(self, obj):
return obj.id in self.context["likes_ids"]
return None
@extend_schema_field(AlbumSerializer)
@extend_schema_field(ListAlbumSerializer)
def get_album(self, obj):
if obj.album:
return AlbumSerializer(Album.objects.cache().get(id=obj.album_id)).data
return ListAlbumSerializer(Album.objects.cache().get(id=obj.album_id)).data
return None
@extend_schema_field(AuthorSerializer(many=True))
@extend_schema_field(ListAuthorSerializer(many=True))
def get_authors(self, obj):
if obj.authors:
return AuthorSerializer(
return ListAuthorSerializer(
Author.objects.cache().filter(songs__id=obj.id), many=True
).data
return None
@ -241,7 +242,7 @@ class FullAlbumSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
artists = serializers.SerializerMethodField("get_artists")
@extend_schema_field(AuthorSerializer(many=True))
@extend_schema_field(ListAuthorSerializer(many=True))
def get_artists(self, obj):
artists = []
qs = Author.objects.cache().filter(
@ -251,14 +252,14 @@ def get_artists(self, obj):
if artist not in artists:
artists.append(artist)
return AuthorSerializer(
return ListAuthorSerializer(
artists,
many=True,
).data
class Meta:
model = Album
fields = ["name", "link", "image", "songs", "artists"]
fields = ["name", "link", "image", "songs", "artists", "meta"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},
@ -269,7 +270,7 @@ class FullAuthorSerializer(serializers.ModelSerializer):
songs = ListSongSerializer(many=True, read_only=True)
albums = serializers.SerializerMethodField(method_name="get_albums")
@extend_schema_field(AlbumSerializer(many=True))
@extend_schema_field(ListAlbumSerializer(many=True))
def get_albums(self, obj):
qs = Album.objects.cache().filter(
songs__id__in=obj.songs.cache().all().values("id").distinct()
@ -280,14 +281,14 @@ def get_albums(self, obj):
if album not in albums:
albums.append(album)
return AlbumSerializer(
return ListAlbumSerializer(
albums,
many=True,
).data
class Meta:
model = Author
fields = ["name", "link", "image", "songs", "albums"]
fields = ["name", "link", "image", "songs", "albums", "meta"]
extra_kwargs = {
"link": {"read_only": True},
"image": {"read_only": True},

View File

@ -6,12 +6,12 @@
from akarpov.common.api.permissions import IsAdminOrReadOnly, IsCreatorOrReadOnly
from akarpov.music.api.serializers import (
AddSongToPlaylistSerializer,
AlbumSerializer,
AuthorSerializer,
FullAlbumSerializer,
FullAuthorSerializer,
FullPlaylistSerializer,
LikeDislikeSongSerializer,
ListAlbumSerializer,
ListAuthorSerializer,
ListPlaylistSerializer,
ListSongSerializer,
PlaylistSerializer,
@ -280,7 +280,7 @@ def get_serializer_context(self, **kwargs):
class ListAlbumsAPIView(generics.ListAPIView):
serializer_class = AlbumSerializer
serializer_class = ListAlbumSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
queryset = Album.objects.cache().all()
@ -297,7 +297,7 @@ class RetrieveUpdateDestroyAlbumAPIView(
class ListAuthorsAPIView(generics.ListAPIView):
serializer_class = AuthorSerializer
serializer_class = ListAuthorSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [permissions.AllowAny]
queryset = Author.objects.cache().all()

View File

@ -10,6 +10,9 @@
from pydub import AudioSegment
from akarpov.music.models import Album, Author, Song
from akarpov.music.services.info import search_all_platforms
from akarpov.users.models import User
from akarpov.utils.generators import generate_charset
def load_track(
@ -23,6 +26,20 @@ def load_track(
**kwargs,
) -> Song:
p_name = path.split("/")[-1]
query = f"{name if name else p_name} - {album if album else ''} - {', '.join(authors) if authors else ''}"
search_info = search_all_platforms(query)
if image_path and search_info.get("album_image", None):
os.remove(search_info["album_image"])
name = name or search_info.get("title", p_name)
album = album or search_info.get("album_name", None)
authors = authors or search_info.get("artists", [])
genre = kwargs.get("genre") or search_info.get("genre", None)
image_path = image_path or search_info.get("album_image", "")
release = (
kwargs["release"] if "release" in kwargs else search_info.get("release", None)
)
if album and type(album) is str and album.startswith("['"):
album = album.replace("['", "").replace("']", "")
@ -81,26 +98,34 @@ def load_track(
)
if user_id:
song.user_id = user_id
song.creator = User.objects.get(id=user_id)
if kwargs:
song.meta = kwargs
if release:
kwargs["release"] = release
new_file_name = (
str(
slugify(
GoogleTranslator(source="auto", target="en").translate(
f"{song.name} {' '.join([x.name for x in authors])}",
target_language="en",
)
kwargs = {
"explicit": kwargs["explicit"] if "explicit" in kwargs else None,
"genre": genre,
"lyrics": kwargs["lyrics"] if "lyrics" in kwargs else None,
"track_source": kwargs["track_source"] if "track_source" in kwargs else None,
} | kwargs
song.meta = kwargs
generated_name = str(
slugify(
GoogleTranslator(source="auto", target="en").translate(
f"{song.name} {' '.join([x.name for x in authors])}",
target_language="en",
)
)
+ ".mp3"
)
new_file_name = generated_name + ".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.image = File(image, name=generated_name + ".png")
song.file = File(file, name=new_file_name)
song.save()
else:
@ -136,9 +161,9 @@ def load_track(
data=f.read(),
)
)
if "release" in kwargs and kwargs["release"]:
if release:
tag.tags.add(TORY(text=kwargs["release"]))
if "genre" in kwargs and kwargs["genre"]:
if genre:
tag.tags.add(TCON(text=kwargs["genre"]))
tag.save()
@ -148,4 +173,19 @@ def load_track(
if os.path.exists(image_path):
os.remove(image_path)
if generated_name and not Song.objects.filter(slug=generated_name).exists():
if len(generated_name) > 20:
generated_name = generated_name.split("-")[0]
if len(generated_name) > 20:
generated_name = generated_name[:20]
if not Song.objects.filter(slug=generated_name).exists():
song.slug = generated_name
song.save()
else:
song.slug = generated_name[:14] + "_" + generate_charset(5)
song.save()
else:
song.slug = generated_name
song.save()
return song

View File

@ -0,0 +1,389 @@
import os
from random import randint
import requests
import spotipy
from deep_translator import GoogleTranslator
from django.conf import settings
from django.core.files import File
from django.utils.text import slugify
from spotipy import SpotifyClientCredentials
from yandex_music import Client, Cover
from akarpov.music.models import Album as AlbumModel
from akarpov.music.models import Author
from akarpov.utils.generators import generate_charset
from akarpov.utils.text import is_similar_artist, normalize_text
def create_spotify_session() -> spotipy.Spotify:
if not settings.MUSIC_SPOTIFY_ID or not settings.MUSIC_SPOTIFY_SECRET:
raise ConnectionError("No spotify credentials provided")
return spotipy.Spotify(
auth_manager=SpotifyClientCredentials(
client_id=settings.MUSIC_SPOTIFY_ID,
client_secret=settings.MUSIC_SPOTIFY_SECRET,
)
)
def yandex_login() -> Client:
if not settings.MUSIC_YANDEX_TOKEN:
raise ConnectionError("No yandex credentials provided")
return Client(settings.MUSIC_YANDEX_TOKEN).init()
def spotify_search(name: str, session: spotipy.Spotify, search_type="track"):
res = session.search(name, type=search_type)
return res
def get_spotify_info(name: str, session: spotipy.Spotify) -> dict:
info = {
"album_name": "",
"album_image": "",
"release": "",
"artists": [],
"artist": "",
"title": "",
"genre": "",
}
try:
results = spotify_search(name, session)["tracks"]["items"]
if not results:
return info
track = results[0]
info.update(
{
"album_name": track["album"]["name"],
"release": track["album"]["release_date"].split("-")[0],
"album_image": track["album"]["images"][0]["url"],
"artists": [artist["name"] for artist in track["artists"]],
"artist": track["artists"][0]["name"],
"title": track["name"],
# Extract additional data as needed
}
)
artist_data = session.artist(track["artists"][0]["external_urls"]["spotify"])
info["genre"] = artist_data.get("genres", [])
album_image_url = track["album"]["images"][0]["url"]
image_response = requests.get(album_image_url)
if image_response.status_code == 200:
image_path = os.path.join(
settings.MEDIA_ROOT, f"tmp_{randint(10000, 99999)}.png"
)
with open(image_path, "wb") as f:
f.write(image_response.content)
info["album_image_path"] = image_path
except Exception:
return info
return info
def search_yandex(name: str):
client = yandex_login()
res = client.search(name, type_="track")
info = {
"album_name": "",
"release": "",
"artists": [],
"title": "",
"genre": "",
}
if res.tracks is None:
return info
if not res.tracks.results:
return info
track = res.tracks.results[0]
info["album_name"] = track.albums[0].title if track.albums else ""
info["release"] = track.albums[0].year if track.albums else ""
info["artists"] = [artist.name for artist in track.artists]
info["title"] = track.title
# try to get genre
if track.albums and track.albums[0].genre:
genre = track.albums[0].genre
elif track.artists and track.artists[0].genres:
genre = track.artists[0].genres[0]
else:
genre = None
info["genre"] = genre
if track.albums and track.albums[0].cover_uri:
cover_uri = track.albums[0].cover_uri.replace("%%", "500x500")
image_response = requests.get("https://" + cover_uri)
if image_response.status_code == 200:
image_path = os.path.join(
settings.MEDIA_ROOT, f"tmp_{randint(10000, 99999)}.png"
)
with open(image_path, "wb") as f:
f.write(image_response.content)
info["album_image_path"] = image_path
return info
def get_spotify_album_info(album_name: str, session: spotipy.Spotify):
search_result = session.search(q="album:" + album_name, type="album")
albums = search_result.get("albums", {}).get("items", [])
if albums:
return albums[0]
return None
def get_spotify_artist_info(artist_name: str, session: spotipy.Spotify):
search_result = session.search(q="artist:" + artist_name, type="artist")
artists = search_result.get("artists", {}).get("items", [])
if artists:
return artists[0]
return None
def get_yandex_album_info(album_name: str, client: Client):
search = client.search(album_name, type_="album")
if search.albums:
return search.albums.results[0]
return None
def get_yandex_artist_info(artist_name: str, client: Client):
search = client.search(artist_name, type_="artist")
if search.artists:
return search.artists.results[0]
return None
def download_image(url, save_path):
if type(url) is Cover:
url = url["uri"]
if not str(url).startswith("http"):
url = "https://" + str(url)
response = requests.get(url)
if response.status_code == 200:
image_path = os.path.join(save_path, f"tmp_{randint(10000, 99999)}.png")
with open(image_path, "wb") as f:
f.write(response.content)
return image_path
return ""
def update_album_info(album: AlbumModel, track_name: str) -> None:
client = yandex_login()
spotify_session = create_spotify_session()
# Retrieve info from both services
yandex_album_info = get_yandex_album_info(album.name + " - " + track_name, client)
spotify_album_info = get_spotify_album_info(
album.name + " - " + track_name, spotify_session
)
# Combine and prioritize Spotify data
album_data = {}
if yandex_album_info:
album_data.update(
{
"name": album_data.get("name", yandex_album_info.title),
"genre": album_data.get("genre", yandex_album_info.genre),
"description": yandex_album_info.description,
"type": yandex_album_info.type,
}
)
if spotify_album_info:
album_data = {
"name": spotify_album_info.get("name", album.name),
"release_date": spotify_album_info.get("release_date", ""),
"total_tracks": spotify_album_info.get("total_tracks", ""),
"link": spotify_album_info["external_urls"]["spotify"],
"genre": spotify_album_info.get("genres", []),
}
album.meta = album_data
album.save()
# Handle Album Image - Prefer Spotify, fallback to Yandex
image_path = None
if (
spotify_album_info
and "images" in spotify_album_info
and spotify_album_info["images"]
):
image_path = download_image(
spotify_album_info["images"][0]["url"], settings.MEDIA_ROOT
)
elif yandex_album_info and yandex_album_info.cover_uri:
image_path = download_image(
"https://" + yandex_album_info.cover_uri, settings.MEDIA_ROOT
)
generated_name = slugify(
GoogleTranslator(source="auto", target="en").translate(
album.name,
target_language="en",
)
)
if image_path:
with open(image_path, "rb") as f:
album.image.save(
generated_name + ".png",
File(
f,
name=generated_name + ".png",
),
save=True,
)
os.remove(image_path)
# Update Album Authors from Spotify data if available
if spotify_album_info and "artists" in spotify_album_info:
album_authors = []
for artist in spotify_album_info["artists"]:
author, created = Author.objects.get_or_create(name=artist["name"])
album_authors.append(author)
album.authors.set(album_authors)
if generated_name and not AlbumModel.objects.filter(slug=generated_name).exists():
if len(generated_name) > 20:
generated_name = generated_name.split("-")[0]
if len(generated_name) > 20:
generated_name = generated_name[:20]
if not AlbumModel.objects.filter(slug=generated_name).exists():
album.slug = generated_name
album.save()
else:
album.slug = generated_name[:14] + "_" + generate_charset(5)
album.save()
else:
album.slug = generated_name
album.save()
def update_author_info(author: Author, track_name: str) -> None:
client = yandex_login()
spotify_session = create_spotify_session()
# Retrieve info from both services
yandex_artist_info = get_yandex_artist_info(
author.name + " - " + track_name, client
)
spotify_artist_info = get_spotify_artist_info(
author.name + " - " + track_name, spotify_session
)
# Combine and prioritize Spotify data
author_data = {}
if yandex_artist_info:
author_data.update(
{
"name": author_data.get("name", yandex_artist_info.name),
"genres": author_data.get("genres", yandex_artist_info.genres),
"description": yandex_artist_info.description,
}
)
if spotify_artist_info:
author_data = {
"name": spotify_artist_info.get("name", author.name),
"genres": spotify_artist_info.get("genres", []),
"popularity": spotify_artist_info.get("popularity", 0),
"link": spotify_artist_info["external_urls"]["spotify"],
}
author.meta = author_data
author.save()
# Handle Author Image - Prefer Spotify, fallback to Yandex
image_path = None
if (
spotify_artist_info
and "images" in spotify_artist_info
and spotify_artist_info["images"]
):
image_path = download_image(
spotify_artist_info["images"][0]["url"], settings.MEDIA_ROOT
)
elif yandex_artist_info and yandex_artist_info.cover:
image_path = download_image(yandex_artist_info.cover, settings.MEDIA_ROOT)
generated_name = slugify(
GoogleTranslator(source="auto", target="en").translate(
author.name,
target_language="en",
)
)
if image_path:
with open(image_path, "rb") as f:
author.image.save(
generated_name + ".png",
File(f, name=generated_name + ".png"),
save=True,
)
os.remove(image_path)
if generated_name and not Author.objects.filter(slug=generated_name).exists():
if len(generated_name) > 20:
generated_name = generated_name.split("-")[0]
if len(generated_name) > 20:
generated_name = generated_name[:20]
if not Author.objects.filter(slug=generated_name).exists():
author.slug = generated_name
author.save()
else:
author.slug = generated_name[:14] + "_" + generate_charset(5)
author.save()
else:
author.slug = generated_name
author.save()
def search_all_platforms(track_name: str) -> dict:
session = spotipy.Spotify(
auth_manager=spotipy.SpotifyClientCredentials(
client_id=settings.MUSIC_SPOTIFY_ID,
client_secret=settings.MUSIC_SPOTIFY_SECRET,
)
)
spotify_info = get_spotify_info(track_name, session)
yandex_info = search_yandex(track_name)
if "album_image_path" in spotify_info and "album_image_path" in yandex_info:
os.remove(yandex_info["album_image_path"])
combined_artists = set()
for artist in spotify_info.get("artists", []) + yandex_info.get("artists", []):
normalized_artist = normalize_text(artist)
if not any(
is_similar_artist(normalized_artist, existing_artist)
for existing_artist in combined_artists
):
combined_artists.add(normalized_artist)
genre = spotify_info.get("genre") or yandex_info.get("genre")
if type(genre) is list:
genre = sorted(genre, key=lambda x: len(x))
genre = genre[0]
track_info = {
"album_name": spotify_info.get("album_name")
or yandex_info.get("album_name", ""),
"release": spotify_info.get("release") or yandex_info.get("release", ""),
"artists": list(combined_artists),
"title": spotify_info.get("title") or yandex_info.get("title", ""),
"genre": genre,
"album_image": spotify_info.get("album_image_path")
or yandex_info.get("album_image_path", None),
}
return track_info

View File

@ -3,9 +3,10 @@
from spotipy.oauth2 import SpotifyClientCredentials
def login() -> spotipy.Spotify:
def create_session() -> spotipy.Spotify:
if not settings.MUSIC_SPOTIFY_ID or not settings.MUSIC_SPOTIFY_SECRET:
raise ConnectionError("No spotify credentials provided")
return spotipy.Spotify(
auth_manager=SpotifyClientCredentials(
client_id=settings.MUSIC_SPOTIFY_ID,
@ -14,40 +15,6 @@ def login() -> spotipy.Spotify:
)
def search(name: str, search_type="track"):
sp = login()
res = sp.search(name, type=search_type)
def search(name: str, session: spotipy.Spotify, search_type="track"):
res = session.search(name, type=search_type)
return res
def get_track_info(name: str) -> dict:
info = {
"album_name": "",
"album_image": "",
"release": "",
"artists": [],
"artist": "",
"title": "",
}
try:
res = search(name)["tracks"]["items"]
except TypeError:
return info
if not res:
return info
res = res[0]
info["album_name"] = res["album"]["name"]
info["release"] = res["album"]["release_date"].split("-")[0]
info["album_image"] = res["album"]["images"][0]["url"]
info["artists"] = [x["name"] for x in res["artists"]]
info["artist"] = [x["name"] for x in res["artists"]][0]
info["title"] = res["name"]
# try to get genre
sp = login()
genres = sp.album(res["album"]["external_urls"]["spotify"])["genres"]
if genres:
info["genre"] = genres[0]
return info

View File

@ -1,14 +1,13 @@
import os
from random import randint
from time import sleep
from django.conf import settings
from django.core.files import File
from yandex_music import Client, Playlist, Search, Track
from yandex_music.exceptions import NotFoundError
from yandex_music import Client, Playlist, Track
from yandex_music.exceptions import NetworkError, NotFoundError
from akarpov.music import tasks
from akarpov.music.models import Album as AlbumModel
from akarpov.music.models import Author, Song, SongInQue
from akarpov.music.models import Song, SongInQue
from akarpov.music.services.db import load_track
@ -18,34 +17,6 @@ def login() -> Client:
return Client(settings.MUSIC_YANDEX_TOKEN).init()
def search_ym(name: str):
client = login()
info = {}
search = client.search(name, type_="track") # type: Search
if search.tracks:
best = search.tracks.results[0] # type: Track
info = {
"artists": [artist.name for artist in best.artists],
"title": best.title,
"album": best.albums[0].title,
}
# getting genre
if best.albums[0].genre:
genre = best.albums[0].genre
elif best.artists[0].genres:
genre = best.artists[0].genres[0]
else:
genre = None
if genre:
info["genre"] = genre
return info
def load_file_meta(track: int, user_id: int) -> str:
que = SongInQue.objects.create()
client = login()
@ -66,8 +37,12 @@ def load_file_meta(track: int, user_id: int) -> str:
filename = f"_{str(randint(10000, 9999999))}"
orig_path = f"{settings.MEDIA_ROOT}/{filename}.mp3"
album = track.albums[0]
try:
track.download(filename=orig_path, codec="mp3")
except NetworkError:
sleep(5)
track.download(filename=orig_path, codec="mp3")
track.download(filename=orig_path, codec="mp3")
img_pth = str(settings.MEDIA_ROOT + f"/_{str(randint(10000, 99999))}.png")
try:
@ -110,64 +85,3 @@ def load_playlist(link: str, user_id: int):
tasks.load_ym_file_meta.apply_async(
kwargs={"track": track.track.id, "user_id": user_id}
)
def update_album_info(album: AlbumModel) -> None:
client = login()
search = client.search(album.name, type_="album") # type: Search
if search.albums:
search_album = search.albums.results[0]
data = {
"name": search_album.title,
"tracks": search_album.track_count,
"explicit": search_album.explicit,
"year": search_album.year,
"genre": search_album.genre,
"description": search_album.description,
"type": search_album.type,
}
album.meta = data
image_path = str(settings.MEDIA_ROOT + f"/_{str(randint(10000, 99999))}.png")
if search_album.cover_uri:
search_album.download_cover(filename=image_path)
with open(image_path, "rb") as f:
album.image = File(f, name=image_path.split("/")[-1])
album.save()
os.remove(image_path)
authors = []
if search_album.artists:
for x in search_album.artists:
try:
authors.append(Author.objects.get(name=x.name))
except Author.DoesNotExist:
authors.append(Author.objects.create(name=x.name))
album.authors.set([x.id for x in authors])
album.save()
def update_author_info(author: Author) -> None:
client = login()
search = client.search(author.name, type_="artist") # type: Search
if search.artists:
search_artist = search.artists.results[0]
data = {
"name": search_artist.name,
"description": search_artist.description,
"genres": search_artist.genres,
}
author.meta = data
image_path = str(settings.MEDIA_ROOT + f"/_{str(randint(10000, 99999))}.png")
if not search_artist.cover:
author.save()
return
search_artist.cover.download(filename=image_path)
with open(image_path, "rb") as f:
author.image = File(f, name=image_path.split("/")[-1])
author.save()
os.remove(image_path)

View File

@ -12,7 +12,7 @@
from akarpov.music.models import Song
from akarpov.music.services.db import load_track
from akarpov.music.services.spotify import get_track_info
from akarpov.music.services.info import search_all_platforms
final_filename = None
@ -99,7 +99,7 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
st = chapters[i][1]
audio = sound[st:]
chapter_path = path.split(".")[0] + chapters[i][2] + ".mp3"
info = get_track_info(chapters[i][2])
info = search_all_platforms(chapters[i][2])
audio.export(chapter_path, format="mp3")
r = requests.get(info["album_image"])
img_pth = str(
@ -136,7 +136,7 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
else:
print(f"[processing] loading {title}")
info = get_track_info(title)
info = search_all_platforms(title)
r = requests.get(info["album_image"])
img_pth = str(
settings.MEDIA_ROOT

View File

@ -4,7 +4,7 @@
from django.dispatch import receiver
from akarpov.music.models import Album, Author, PlaylistSong, Song, SongUserRating
from akarpov.music.services.yandex import update_album_info, update_author_info
from akarpov.music.services.info import update_album_info, update_author_info
@receiver(post_delete, sender=Song)
@ -17,13 +17,15 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Author)
def author_create(sender, instance, created, **kwargs):
if created:
update_author_info(instance)
songs = Song.objects.filter(authors=instance)
update_author_info(instance, songs.first().name if songs.exists() else "")
@receiver(post_save, sender=Album)
def album_create(sender, instance, created, **kwargs):
if created:
update_album_info(instance)
songs = Song.objects.filter(album=instance)
update_album_info(instance, songs.first().name if songs.exists() else "")
@receiver(post_save)

10
akarpov/utils/text.py Normal file
View File

@ -0,0 +1,10 @@
from fuzzywuzzy import fuzz
from unidecode import unidecode
def normalize_text(text):
return unidecode(text.lower().strip())
def is_similar_artist(name1, name2, threshold=90):
return fuzz.ratio(normalize_text(name1), normalize_text(name2)) > threshold

490
poetry.lock generated
View File

@ -1286,6 +1286,19 @@ files = [
docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
tests = ["pytest", "pytest-cov", "pytest-xdist"]
[[package]]
name = "dacite"
version = "1.8.1"
description = "Simple creation of data classes from dictionaries."
optional = false
python-versions = ">=3.6"
files = [
{file = "dacite-1.8.1-py3-none-any.whl", hash = "sha256:cc31ad6fdea1f49962ea42db9421772afe01ac5442380d9a99fcf3d188c61afe"},
]
[package.extras]
dev = ["black", "coveralls", "mypy", "pre-commit", "pylint", "pytest (>=5)", "pytest-benchmark", "pytest-cov"]
[[package]]
name = "daphne"
version = "4.0.0"
@ -1353,6 +1366,23 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "deprecated"
version = "1.2.14"
description = "Python @deprecated decorator to deprecate old python classes, functions or methods."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
files = [
{file = "Deprecated-1.2.14-py2.py3-none-any.whl", hash = "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c"},
{file = "Deprecated-1.2.14.tar.gz", hash = "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3"},
]
[package.dependencies]
wrapt = ">=1.10,<2"
[package.extras]
dev = ["PyTest", "PyTest-Cov", "bump2version (<1)", "sphinx (<2)", "tox"]
[[package]]
name = "dill"
version = "0.3.7"
@ -2190,20 +2220,19 @@ python-dateutil = ">=2.4"
[[package]]
name = "fastapi"
version = "0.104.1"
version = "0.103.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"},
{file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"},
{file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"},
{file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"},
]
[package.dependencies]
anyio = ">=3.7.1,<4.0.0"
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
starlette = ">=0.27.0,<0.28.0"
typing-extensions = ">=4.8.0"
typing-extensions = ">=4.5.0"
[package.extras]
all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
@ -2465,6 +2494,20 @@ files = [
{file = "future-0.18.3.tar.gz", hash = "sha256:34a17436ed1e96697a86f9de3d15a3b0be01d8bc8de9c1dffd59fb8234ed5307"},
]
[[package]]
name = "fuzzywuzzy"
version = "0.18.0"
description = "Fuzzy string matching in python"
optional = false
python-versions = "*"
files = [
{file = "fuzzywuzzy-0.18.0-py2.py3-none-any.whl", hash = "sha256:928244b28db720d1e0ee7587acf660ea49d7e4c632569cad4f1cd7e68a5f0993"},
{file = "fuzzywuzzy-0.18.0.tar.gz", hash = "sha256:45016e92264780e58972dca1b3d939ac864b78437422beecebb3095f8efd00e8"},
]
[package.extras]
speedup = ["python-levenshtein (>=0.12)"]
[[package]]
name = "greenlet"
version = "3.0.3"
@ -2882,6 +2925,16 @@ files = [
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "jaconv"
version = "0.3.4"
description = "Pure-Python Japanese character interconverter for Hiragana, Katakana, Hankaku, Zenkaku and more"
optional = false
python-versions = "*"
files = [
{file = "jaconv-0.3.4.tar.gz", hash = "sha256:9e7c55f3f0b0e2dbad62f6c9fa0c30fc6fffdbb78297955509d90856b3a31d6d"},
]
[[package]]
name = "jedi"
version = "0.19.1"
@ -3140,6 +3193,126 @@ interegular = ["interegular (>=0.3.1,<0.4.0)"]
nearley = ["js2py"]
regex = ["regex"]
[[package]]
name = "levenshtein"
version = "0.23.0"
description = "Python extension for computing string edit distances and similarities."
optional = false
python-versions = ">=3.7"
files = [
{file = "Levenshtein-0.23.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2d3f2b8e67915268c49f0faa29a29a8c26811a4b46bd96dd043bc8557428065d"},
{file = "Levenshtein-0.23.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:10b980dcc865f8fe04723e448fac4e9a32cbd21fb41ab548725a2d30d9a22429"},
{file = "Levenshtein-0.23.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2f8c8c48217b2733ae5bd8ef14e0ad730a30d113c84dc2cfc441435ef900732b"},
{file = "Levenshtein-0.23.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:854a0962d6f5852b891b6b5789467d1e72b69722df1bc0dd85cbf70efeddc83f"},
{file = "Levenshtein-0.23.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5abc4ee22340625ec401d6f11136afa387d377b7aa5dad475618ffce1f0d2e2f"},
{file = "Levenshtein-0.23.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:20f79946481052bbbee5284c755aa0a5feb10a344d530e014a50cb9544745dd3"},
{file = "Levenshtein-0.23.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6084fc909a218843bb55723fde64a8a58bac7e9086854c37134269b3f946aeb"},
{file = "Levenshtein-0.23.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0acaae1c20c8ed37915b0cde14b5c77d5a3ba08e05f9ce4f55e16843de9c7bb8"},
{file = "Levenshtein-0.23.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:54a51036b02222912a029a6efa2ce1ee2be49c88e0bb32995e0999feba183913"},
{file = "Levenshtein-0.23.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:68ec2ef442621027f290cb5cef80962889d86fff3e405e5d21c7f9634d096bbf"},
{file = "Levenshtein-0.23.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d8ba18720bafa4a65f07baba8c3228e98a6f8da7455de4ec58ae06de4ecdaea0"},
{file = "Levenshtein-0.23.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:af1b70cac87c5627cd2227823318fa39c64fbfed686c8c3c2f713f72bc25813b"},
{file = "Levenshtein-0.23.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fe2810c42cc5bca15eeb4a2eb192b1f74ceef6005876b1a166ecbde1defbd22d"},
{file = "Levenshtein-0.23.0-cp310-cp310-win32.whl", hash = "sha256:89a0829637221ff0fd6ce63dfbe59e22b25eeba914d50e191519b9d9b8ccf3e9"},
{file = "Levenshtein-0.23.0-cp310-cp310-win_amd64.whl", hash = "sha256:b8bc81d59205558326ac75c97e236fd72b8bcdf63fcdbfb7387bd63da242b209"},
{file = "Levenshtein-0.23.0-cp310-cp310-win_arm64.whl", hash = "sha256:151046d1c70bdf01ede01f46467c11151ceb9c86fefaf400978b990110d0a55e"},
{file = "Levenshtein-0.23.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7e992de09832ee11b35910c05c1581e8a9ab8ea9737c2f582c7eb540e2cdde69"},
{file = "Levenshtein-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a5e3461d29b3188518464bd3121fc64635ff884ae544147b5d326ce13c50d36"},
{file = "Levenshtein-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1772c4491f6ef6504e591c0dd60e1e418b2015074c3d56ee93af6b1a019906ee"},
{file = "Levenshtein-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e125c92cd0ac3b53c4c80fcf2890d89a1d19ff4979dc804031773bc90223859f"},
{file = "Levenshtein-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d2f608c5ce7b9a0a0af3c910f43ea7eb060296655aa127b10e4af7be5559303"},
{file = "Levenshtein-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe5c3b7d96a838d9d86bb4ec57495749965e598a3ea2c5b877a61aa09478bab7"},
{file = "Levenshtein-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249eaa351b5355b3e3ca7e3a8e2a0bca7bff4491c89a0b0fa3b9d0614cf3efeb"},
{file = "Levenshtein-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0033a243510e829ead1ae62720389c9f17d422a98c0525da593d239a9ff434e5"},
{file = "Levenshtein-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f956ad16cab9267c0e7d382a37b4baca6bf3bf1637a76fa95fdbf9dd3ea774d7"},
{file = "Levenshtein-0.23.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:3789e4aeaeb830d944e1f502f9aa9024e9cd36b68d6eba6892df7972b884abd7"},
{file = "Levenshtein-0.23.0-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f91335f056b9a548070cb87b3e6cf017a18b27d34a83f222bdf46a5360615f11"},
{file = "Levenshtein-0.23.0-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3497eda857e70863a090673a82442877914c57b5f04673c782642e69caf25c0c"},
{file = "Levenshtein-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5e17ea59115179c269c6daea52415faaf54c6340d4ad91d9012750845a445a13"},
{file = "Levenshtein-0.23.0-cp311-cp311-win32.whl", hash = "sha256:da2063cee1fbecc09e1692e7c4de7624fd4c47a54ee7588b7ea20540f8f8d779"},
{file = "Levenshtein-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:4d3b9c9e2852eca20de6bd8ca7f47d817a056993fd4927a4d50728b62315376b"},
{file = "Levenshtein-0.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:ef2e3e93ae612ac87c3a28f08e8544b707d67e99f9624e420762a7c275bb13c5"},
{file = "Levenshtein-0.23.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:85220b27a47df4a5106ef13d43b6181d73da77d3f78646ec7251a0c5eb08ac40"},
{file = "Levenshtein-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bb77b3ade7f256ca5882450aaf129be79b11e074505b56c5997af5058a8f834"},
{file = "Levenshtein-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b487f08c32530ee608e8aab0c4075048262a7f5a6e113bac495b05154ae427"},
{file = "Levenshtein-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f91d0a5d3696e373cae08c80ec99a4ff041e562e55648ebe582725cba555190"},
{file = "Levenshtein-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fddda71ae372cd835ffd64990f0d0b160409e881bf8722b6c5dc15dc4239d7db"},
{file = "Levenshtein-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7664bcf9a12e62c672a926c4579f74689507beaa24378ad7664f0603b0dafd20"},
{file = "Levenshtein-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6d07539502610ee8d6437a77840feedefa47044ab0f35cd3bc37adfc63753bd"},
{file = "Levenshtein-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:830a74b6a045a13e1b1d28af62af9878aeae8e7386f14888c84084d577b92771"},
{file = "Levenshtein-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f29cbd0c172a8fc1d51eaacd163bdc11596aded5a90db617e6b778c2258c7006"},
{file = "Levenshtein-0.23.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:df0704fd6a30a7c27c03655ae6dc77345c1655634fe59654e74bb06a3c7c1357"},
{file = "Levenshtein-0.23.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:0ab52358f54ee48ad7656a773a0c72ef89bb9ba5acc6b380cfffd619fb223a23"},
{file = "Levenshtein-0.23.0-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:f0a86394c9440e23a29f48f2bbc460de7b19950f46ec2bea3be8c2090839bb29"},
{file = "Levenshtein-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a689e6e0514f48a434e7ee44cc1eb29c34b21c51c57accb304eac97fba87bf48"},
{file = "Levenshtein-0.23.0-cp312-cp312-win32.whl", hash = "sha256:2d3229c1336498c2b72842dd4c850dff1040588a5468abe5104444a372c1a573"},
{file = "Levenshtein-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:5b9b6a8509415bc214d33f5828d7c700c80292ea25f9d9e8cba95ad5a74b3cdf"},
{file = "Levenshtein-0.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:5a61606bad3afb9fcec0a2a21871319c3f7da933658d2e0e6e55ab4a34814f48"},
{file = "Levenshtein-0.23.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:078bb87ea32a28825900f5d29ba2946dc9cf73094dfed4ba5d70f042f2435609"},
{file = "Levenshtein-0.23.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26b468455f29fb255b62c22522026985cb3181a02e570c8b37659fedb1bc0170"},
{file = "Levenshtein-0.23.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3dc62b2f74e4050f0a1261a34e11fd9e7c6d80a45679c0e02ac452b16fda7b34"},
{file = "Levenshtein-0.23.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b65b0b4e8b88e8326cdbfd3ec119953a0b10b514947f4bd03a4ed0fc58f6471"},
{file = "Levenshtein-0.23.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bccaf7f16b9da5edb608705edc3c38401e83ea0ff04c6375f25c6fc15e88f9b3"},
{file = "Levenshtein-0.23.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6b35f752d04c0828fb1877d9bee5d1786b2574ec3b1cba0533008aa1ff203712"},
{file = "Levenshtein-0.23.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:2c32f86bb54b9744c95c27b5398f108158cc6a87c5dbb3ad5a344634bf9b07d3"},
{file = "Levenshtein-0.23.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:fa8b65f483cdd3114d41736e0e9c3841e7ee6ac5861bae3d26e21e19faa229ff"},
{file = "Levenshtein-0.23.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:9fdf67c10a5403b1668d1b6ade7744d20790367b10866d27394e64716992c3e4"},
{file = "Levenshtein-0.23.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:eb6dfba3264b38a3e95cac8e64f318ad4c27e2232f6c566a69b3b113115c06ef"},
{file = "Levenshtein-0.23.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8541f1b7516290f6ccc3faac9aea681183c5d0b1f8078b957ae41dfbd5b93b58"},
{file = "Levenshtein-0.23.0-cp37-cp37m-win32.whl", hash = "sha256:f35b138bb698b29467627318af9258ec677e021e0816ae0da9b84f9164ed7518"},
{file = "Levenshtein-0.23.0-cp37-cp37m-win_amd64.whl", hash = "sha256:936320113eadd3d71d9ce371d9027b1c56299001b48ed197a0db4140e1d13bbd"},
{file = "Levenshtein-0.23.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:da64e19e1ec0c1e8a1cd77c4802a0d656f8a6e0ab7a1479d435a9d2575e473f8"},
{file = "Levenshtein-0.23.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e729781b6134a6e3b380a2d8eae0843a230fc3716bdc8bba4cde2b0ce260982b"},
{file = "Levenshtein-0.23.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97d0841a2682a3c302f70537e8316077e56795062c6f629714f5d0771f7a5838"},
{file = "Levenshtein-0.23.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:727a679d19b18a0b4532abf87f9788070bcd94b78ff07135abe41c716bccbb7d"},
{file = "Levenshtein-0.23.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48c8388a321e55c1feeef543b49fc969be6a5cf6bcf4dcb5dced82f5fea6793c"},
{file = "Levenshtein-0.23.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58f8b8f5d4348e470e8c0d4e9f7c23a8f7cfc3cbd8024cc5a1fc68cc81f7d6cb"},
{file = "Levenshtein-0.23.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:549170257f052289df93a13526877cb397d351b0c8a3e4c9ae3936aeafd8ad17"},
{file = "Levenshtein-0.23.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5d32f3b28065e430d54781e1f3b31198b6bfc21e6d565f0c06218e7618884551"},
{file = "Levenshtein-0.23.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ecc8c12e710212c4d959fda3a52377ae6a30fa204822f2e63fd430e018be3d6f"},
{file = "Levenshtein-0.23.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:88b47fbabbd9cee8be5d6c26ac4d599dd66146628b9ca23d9f4f209c4e3e143e"},
{file = "Levenshtein-0.23.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:5106bce4e94bc1ae137b50d1e5f49b726997be879baf66eafc6ee365adec3db5"},
{file = "Levenshtein-0.23.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:d36634491e06234672492715bc6ff7be61aeaf44822cb366dbbe9d924f2614cc"},
{file = "Levenshtein-0.23.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a591c94f7047d105c29630e7606a2b007f96cf98651fb93e9f820272b0361e02"},
{file = "Levenshtein-0.23.0-cp38-cp38-win32.whl", hash = "sha256:9fce199af18d459c8f19747501d1e852d86550162e7ccdc2c193b44e55d9bbfb"},
{file = "Levenshtein-0.23.0-cp38-cp38-win_amd64.whl", hash = "sha256:b4303024ffea56fd164a68f80f23df9e9158620593b7515c73c885285ec6a558"},
{file = "Levenshtein-0.23.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:73aed4856e672ab12769472cf7aece04b4a6813eb917390d22e58002576136e0"},
{file = "Levenshtein-0.23.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4e93dbfdf08360b4261a2385340d26ac491a1bf9bd17bf22a59636705d2d6479"},
{file = "Levenshtein-0.23.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b847f716fc314cf83d128fedc2c16ffdff5431a439db412465c4b0ac1762478e"},
{file = "Levenshtein-0.23.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0d567beb47cd403394bf241df8cfc14499279d0f3a6675f89b667249841aab1"},
{file = "Levenshtein-0.23.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e13857d870048ff58ce95c8eb32e10285918ee74e1c9bf1825af08dd49b0bc6"},
{file = "Levenshtein-0.23.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c4250f507bb1b7501f7187af8345e200cbc1a58ceb3730bf4e3fdc371fe732c0"},
{file = "Levenshtein-0.23.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fb90de8a279ce83797bcafbbfe6d641362c3c96148c17d8c8612dddb02744c5"},
{file = "Levenshtein-0.23.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:039dc7323fd28de44d6c13a334a34ab1ddee598762cb2dae3223ca1f083577f9"},
{file = "Levenshtein-0.23.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d5739f513cb02039f970054eabeccc62696ed2a1afff6e17f75d5492a3ed8d74"},
{file = "Levenshtein-0.23.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2a3801a0463791440b4350b734e4ec0dbc140b675a3ce9ef936feed06b23c58d"},
{file = "Levenshtein-0.23.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:606ba30bbdf06fc51b0a763760e113dea9085011a2399cf4b1f72316836e4d03"},
{file = "Levenshtein-0.23.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:14c5f90859e512004cc25b50b79c7ae6f068ebe69a7213a9018c83bd88c1305b"},
{file = "Levenshtein-0.23.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c8a75233798e334fd53305656ffcf0601f60e9ff461af759677006c07c060939"},
{file = "Levenshtein-0.23.0-cp39-cp39-win32.whl", hash = "sha256:9a271d50643cf927bfc002d397b4f715abdbc6ca46a5a93d1d66a033eabaa5f3"},
{file = "Levenshtein-0.23.0-cp39-cp39-win_amd64.whl", hash = "sha256:684118d9e070e00df91bc4bd276e0559df7bb2319659699dafda16b5a0229553"},
{file = "Levenshtein-0.23.0-cp39-cp39-win_arm64.whl", hash = "sha256:98412a7bdc49c7fbb493be3c3e7fd2f874eff29ed636b8c0eca325a1e3e74264"},
{file = "Levenshtein-0.23.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:760c964ff0be8dea5f7eda20314cf66238fdd0fec63f1ce9c474736bb2904924"},
{file = "Levenshtein-0.23.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de42400ea86e3e8be3dc7f9b3b9ed51da7fd06dc2f3a426d7effd7fbf35de848"},
{file = "Levenshtein-0.23.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2080ee52aeac03854a0c6e73d4214d5be2120bdd5f16def4394f9fbc5666e04"},
{file = "Levenshtein-0.23.0-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb00ecae116e62801613788d8dc3938df26f582efce5a3d3320e9692575e7c4d"},
{file = "Levenshtein-0.23.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:f351694f65d4df48ee2578d977d37a0560bd3e8535e85dfe59df6abeed12bd6e"},
{file = "Levenshtein-0.23.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:34859c5ff7261f25daea810b5439ad80624cbb9021381df2c390c20eb75b79c6"},
{file = "Levenshtein-0.23.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ece1d077d9006cff329bb95eb9704f407933ff4484e5d008a384d268b993439"},
{file = "Levenshtein-0.23.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35ce82403730dd2a3b397abb2535786af06835fcf3dc40dc8ea67ed589bbd010"},
{file = "Levenshtein-0.23.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a88aa3b5f49aeca08080b6c3fa7e1095d939eafb13f42dbe8f1b27ff405fd43"},
{file = "Levenshtein-0.23.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:748fbba6d9c04fc39b956b44ccde8eb14f34e21ab68a0f9965aae3fa5c8fdb5e"},
{file = "Levenshtein-0.23.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:60440d583986e344119a15cea9e12099f3a07bdddc1c98ec2dda69e96429fb25"},
{file = "Levenshtein-0.23.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b048a83b07fc869648460f2af1255e265326d75965157a165dde2d9ba64fa73"},
{file = "Levenshtein-0.23.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4be0e5e742f6a299acf7aa8d2e5cfca946bcff224383fd451d894e79499f0a46"},
{file = "Levenshtein-0.23.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7a626637c1d967e3e504ced353f89c2a9f6c8b4b4dbf348fdd3e1daa947a23c"},
{file = "Levenshtein-0.23.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:88d8a13cf310cfc893e3734f8e7e42ef20c52780506e9bdb96e76a8b75e3ba20"},
{file = "Levenshtein-0.23.0.tar.gz", hash = "sha256:de7ccc31a471ea5bfafabe804c12a63e18b4511afc1014f23c3cc7be8c70d3bd"},
]
[package.dependencies]
rapidfuzz = ">=3.1.0,<4.0.0"
[[package]]
name = "livereload"
version = "2.6.3"
@ -3297,6 +3470,30 @@ files = [
docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
testing = ["coverage", "pyyaml"]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
[[package]]
name = "markupsafe"
version = "2.1.3"
@ -3429,6 +3626,17 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "msgpack"
version = "1.0.7"
@ -3956,13 +4164,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]]
name = "platformdirs"
version = "4.1.0"
version = "3.11.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.7"
files = [
{file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"},
{file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"},
{file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
]
[package.extras]
@ -4520,6 +4728,26 @@ dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pyte
docs = ["sphinx (>=4.5.0,<5.0.0)", "sphinx-rtd-theme", "zope.interface"]
tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
[[package]]
name = "pykakasi"
version = "2.2.1"
description = "Kana kanji simple inversion library"
optional = false
python-versions = "*"
files = [
{file = "pykakasi-2.2.1-py3-none-any.whl", hash = "sha256:f2d3d12a684314d7f317314499f5b0bec4a711eef4cfc963a2ca6f5c3d68f3b3"},
{file = "pykakasi-2.2.1.tar.gz", hash = "sha256:3a3510929a5596cae51fffa9cf78c0f742d96cebd93f726c96acee51407d18cc"},
]
[package.dependencies]
deprecated = "*"
jaconv = "*"
[package.extras]
check = ["check-manifest", "docutils", "flake8", "flake8-black", "flake8-deprecated", "isort", "mypy (==0.770)", "mypy-extensions (==0.4.3)", "pygments", "readme-renderer", "twine"]
docs = ["sphinx (>=1.8)", "sphinx-intl", "sphinx-py3doc-enhanced-theme", "sphinx-rtd-theme"]
test = ["coverage[toml] (>=5.2)", "py-cpuinfo", "pytest", "pytest-benchmark", "pytest-cov", "pytest-pep8", "pytest-profiling"]
[[package]]
name = "pylint"
version = "3.0.3"
@ -4869,6 +5097,20 @@ files = [
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-levenshtein"
version = "0.23.0"
description = "Python extension for computing string edit distances and similarities."
optional = false
python-versions = ">=3.7"
files = [
{file = "python-Levenshtein-0.23.0.tar.gz", hash = "sha256:156a0198cdcc659c90c8d3863d0ed3f4f0cf020608da71da52ac0f0746ef901a"},
{file = "python_Levenshtein-0.23.0-py3-none-any.whl", hash = "sha256:486a47b189e3955463107aa36b57fb1e2b3b40243b9cc2994cde9810c78195c0"},
]
[package.dependencies]
Levenshtein = "0.23.0"
[[package]]
name = "python-magic"
version = "0.4.27"
@ -4912,17 +5154,18 @@ XlsxWriter = ">=0.5.7"
[[package]]
name = "python-slugify"
version = "7.0.0"
version = "8.0.1"
description = "A Python slugify application that also handles Unicode"
optional = false
python-versions = ">=3.7"
files = [
{file = "python-slugify-7.0.0.tar.gz", hash = "sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b"},
{file = "python_slugify-7.0.0-py2.py3-none-any.whl", hash = "sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570"},
{file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"},
{file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"},
]
[package.dependencies]
text-unidecode = ">=1.3"
Unidecode = {version = ">=1.1.1", optional = true, markers = "extra == \"unidecode\""}
[package.extras]
unidecode = ["Unidecode (>=1.1.1)"]
@ -5040,6 +5283,108 @@ maintainer = ["zest.releaser[recommended]"]
pil = ["pillow (>=9.1.0)"]
test = ["coverage", "pytest"]
[[package]]
name = "rapidfuzz"
version = "3.6.1"
description = "rapid fuzzy string matching"
optional = false
python-versions = ">=3.8"
files = [
{file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ac434fc71edda30d45db4a92ba5e7a42c7405e1a54cb4ec01d03cc668c6dcd40"},
{file = "rapidfuzz-3.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2a791168e119cfddf4b5a40470620c872812042f0621e6a293983a2d52372db0"},
{file = "rapidfuzz-3.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5a2f3e9df346145c2be94e4d9eeffb82fab0cbfee85bd4a06810e834fe7c03fa"},
{file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23de71e7f05518b0bbeef55d67b5dbce3bcd3e2c81e7e533051a2e9401354eb0"},
{file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d056e342989248d2bdd67f1955bb7c3b0ecfa239d8f67a8dfe6477b30872c607"},
{file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01835d02acd5d95c1071e1da1bb27fe213c84a013b899aba96380ca9962364bc"},
{file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed0f712e0bb5fea327e92aec8a937afd07ba8de4c529735d82e4c4124c10d5a0"},
{file = "rapidfuzz-3.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96cd19934f76a1264e8ecfed9d9f5291fde04ecb667faef5f33bdbfd95fe2d1f"},
{file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e06c4242a1354cf9d48ee01f6f4e6e19c511d50bb1e8d7d20bcadbb83a2aea90"},
{file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d73dcfe789d37c6c8b108bf1e203e027714a239e50ad55572ced3c004424ed3b"},
{file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e98ff000e2619e7cfe552d086815671ed09b6899408c2c1b5103658261f6f3"},
{file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:08b6fb47dd889c69fbc0b915d782aaed43e025df6979b6b7f92084ba55edd526"},
{file = "rapidfuzz-3.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a1788ebb5f5b655a15777e654ea433d198f593230277e74d51a2a1e29a986283"},
{file = "rapidfuzz-3.6.1-cp310-cp310-win32.whl", hash = "sha256:c65f92881753aa1098c77818e2b04a95048f30edbe9c3094dc3707d67df4598b"},
{file = "rapidfuzz-3.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:4243a9c35667a349788461aae6471efde8d8800175b7db5148a6ab929628047f"},
{file = "rapidfuzz-3.6.1-cp310-cp310-win_arm64.whl", hash = "sha256:f59d19078cc332dbdf3b7b210852ba1f5db8c0a2cd8cc4c0ed84cc00c76e6802"},
{file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:fbc07e2e4ac696497c5f66ec35c21ddab3fc7a406640bffed64c26ab2f7ce6d6"},
{file = "rapidfuzz-3.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:40cced1a8852652813f30fb5d4b8f9b237112a0bbaeebb0f4cc3611502556764"},
{file = "rapidfuzz-3.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:82300e5f8945d601c2daaaac139d5524d7c1fdf719aa799a9439927739917460"},
{file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edf97c321fd641fea2793abce0e48fa4f91f3c202092672f8b5b4e781960b891"},
{file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7420e801b00dee4a344ae2ee10e837d603461eb180e41d063699fb7efe08faf0"},
{file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:060bd7277dc794279fa95522af355034a29c90b42adcb7aa1da358fc839cdb11"},
{file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7e3375e4f2bfec77f907680328e4cd16cc64e137c84b1886d547ab340ba6928"},
{file = "rapidfuzz-3.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a490cd645ef9d8524090551016f05f052e416c8adb2d8b85d35c9baa9d0428ab"},
{file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:2e03038bfa66d2d7cffa05d81c2f18fd6acbb25e7e3c068d52bb7469e07ff382"},
{file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:2b19795b26b979c845dba407fe79d66975d520947b74a8ab6cee1d22686f7967"},
{file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:064c1d66c40b3a0f488db1f319a6e75616b2e5fe5430a59f93a9a5e40a656d15"},
{file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:3c772d04fb0ebeece3109d91f6122b1503023086a9591a0b63d6ee7326bd73d9"},
{file = "rapidfuzz-3.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:841eafba6913c4dfd53045835545ba01a41e9644e60920c65b89c8f7e60c00a9"},
{file = "rapidfuzz-3.6.1-cp311-cp311-win32.whl", hash = "sha256:266dd630f12696ea7119f31d8b8e4959ef45ee2cbedae54417d71ae6f47b9848"},
{file = "rapidfuzz-3.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:d79aec8aeee02ab55d0ddb33cea3ecd7b69813a48e423c966a26d7aab025cdfe"},
{file = "rapidfuzz-3.6.1-cp311-cp311-win_arm64.whl", hash = "sha256:484759b5dbc5559e76fefaa9170147d1254468f555fd9649aea3bad46162a88b"},
{file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b2ef4c0fd3256e357b70591ffb9e8ed1d439fb1f481ba03016e751a55261d7c1"},
{file = "rapidfuzz-3.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:588c4b20fa2fae79d60a4e438cf7133d6773915df3cc0a7f1351da19eb90f720"},
{file = "rapidfuzz-3.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7142ee354e9c06e29a2636b9bbcb592bb00600a88f02aa5e70e4f230347b373e"},
{file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1dfc557c0454ad22382373ec1b7df530b4bbd974335efe97a04caec936f2956a"},
{file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:03f73b381bdeccb331a12c3c60f1e41943931461cdb52987f2ecf46bfc22f50d"},
{file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b0ccc2ec1781c7e5370d96aef0573dd1f97335343e4982bdb3a44c133e27786"},
{file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da3e8c9f7e64bb17faefda085ff6862ecb3ad8b79b0f618a6cf4452028aa2222"},
{file = "rapidfuzz-3.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fde9b14302a31af7bdafbf5cfbb100201ba21519be2b9dedcf4f1048e4fbe65d"},
{file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1a23eee225dfb21c07f25c9fcf23eb055d0056b48e740fe241cbb4b22284379"},
{file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:e49b9575d16c56c696bc7b06a06bf0c3d4ef01e89137b3ddd4e2ce709af9fe06"},
{file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:0a9fc714b8c290261669f22808913aad49553b686115ad0ee999d1cb3df0cd66"},
{file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:a3ee4f8f076aa92184e80308fc1a079ac356b99c39408fa422bbd00145be9854"},
{file = "rapidfuzz-3.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f056ba42fd2f32e06b2c2ba2443594873cfccc0c90c8b6327904fc2ddf6d5799"},
{file = "rapidfuzz-3.6.1-cp312-cp312-win32.whl", hash = "sha256:5d82b9651e3d34b23e4e8e201ecd3477c2baa17b638979deeabbb585bcb8ba74"},
{file = "rapidfuzz-3.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:dad55a514868dae4543ca48c4e1fc0fac704ead038dafedf8f1fc0cc263746c1"},
{file = "rapidfuzz-3.6.1-cp312-cp312-win_arm64.whl", hash = "sha256:3c84294f4470fcabd7830795d754d808133329e0a81d62fcc2e65886164be83b"},
{file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e19d519386e9db4a5335a4b29f25b8183a1c3f78cecb4c9c3112e7f86470e37f"},
{file = "rapidfuzz-3.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:01eb03cd880a294d1bf1a583fdd00b87169b9cc9c9f52587411506658c864d73"},
{file = "rapidfuzz-3.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:be368573255f8fbb0125a78330a1a40c65e9ba3c5ad129a426ff4289099bfb41"},
{file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3e5af946f419c30f5cb98b69d40997fe8580efe78fc83c2f0f25b60d0e56efb"},
{file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f382f7ffe384ce34345e1c0b2065451267d3453cadde78946fbd99a59f0cc23c"},
{file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be156f51f3a4f369e758505ed4ae64ea88900dcb2f89d5aabb5752676d3f3d7e"},
{file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1936d134b6c513fbe934aeb668b0fee1ffd4729a3c9d8d373f3e404fbb0ce8a0"},
{file = "rapidfuzz-3.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:12ff8eaf4a9399eb2bebd838f16e2d1ded0955230283b07376d68947bbc2d33d"},
{file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae598a172e3a95df3383634589660d6b170cc1336fe7578115c584a99e0ba64d"},
{file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cd4ba4c18b149da11e7f1b3584813159f189dc20833709de5f3df8b1342a9759"},
{file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:0402f1629e91a4b2e4aee68043a30191e5e1b7cd2aa8dacf50b1a1bcf6b7d3ab"},
{file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:1e12319c6b304cd4c32d5db00b7a1e36bdc66179c44c5707f6faa5a889a317c0"},
{file = "rapidfuzz-3.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:0bbfae35ce4de4c574b386c43c78a0be176eeddfdae148cb2136f4605bebab89"},
{file = "rapidfuzz-3.6.1-cp38-cp38-win32.whl", hash = "sha256:7fec74c234d3097612ea80f2a80c60720eec34947066d33d34dc07a3092e8105"},
{file = "rapidfuzz-3.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:a553cc1a80d97459d587529cc43a4c7c5ecf835f572b671107692fe9eddf3e24"},
{file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:757dfd7392ec6346bd004f8826afb3bf01d18a723c97cbe9958c733ab1a51791"},
{file = "rapidfuzz-3.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2963f4a3f763870a16ee076796be31a4a0958fbae133dbc43fc55c3968564cf5"},
{file = "rapidfuzz-3.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d2f0274595cc5b2b929c80d4e71b35041104b577e118cf789b3fe0a77b37a4c5"},
{file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f211e366e026de110a4246801d43a907cd1a10948082f47e8a4e6da76fef52"},
{file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a59472b43879012b90989603aa5a6937a869a72723b1bf2ff1a0d1edee2cc8e6"},
{file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a03863714fa6936f90caa7b4b50ea59ea32bb498cc91f74dc25485b3f8fccfe9"},
{file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd95b6b7bfb1584f806db89e1e0c8dbb9d25a30a4683880c195cc7f197eaf0c"},
{file = "rapidfuzz-3.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7183157edf0c982c0b8592686535c8b3e107f13904b36d85219c77be5cefd0d8"},
{file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ad9d74ef7c619b5b0577e909582a1928d93e07d271af18ba43e428dc3512c2a1"},
{file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b53137d81e770c82189e07a8f32722d9e4260f13a0aec9914029206ead38cac3"},
{file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:49b9ed2472394d306d5dc967a7de48b0aab599016aa4477127b20c2ed982dbf9"},
{file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:dec307b57ec2d5054d77d03ee4f654afcd2c18aee00c48014cb70bfed79597d6"},
{file = "rapidfuzz-3.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4381023fa1ff32fd5076f5d8321249a9aa62128eb3f21d7ee6a55373e672b261"},
{file = "rapidfuzz-3.6.1-cp39-cp39-win32.whl", hash = "sha256:8d7a072f10ee57c8413c8ab9593086d42aaff6ee65df4aa6663eecdb7c398dca"},
{file = "rapidfuzz-3.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:ebcfb5bfd0a733514352cfc94224faad8791e576a80ffe2fd40b2177bf0e7198"},
{file = "rapidfuzz-3.6.1-cp39-cp39-win_arm64.whl", hash = "sha256:1c47d592e447738744905c18dda47ed155620204714e6df20eb1941bb1ba315e"},
{file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:eef8b346ab331bec12bbc83ac75641249e6167fab3d84d8f5ca37fd8e6c7a08c"},
{file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53251e256017e2b87f7000aee0353ba42392c442ae0bafd0f6b948593d3f68c6"},
{file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6dede83a6b903e3ebcd7e8137e7ff46907ce9316e9d7e7f917d7e7cdc570ee05"},
{file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4da90e4c2b444d0a171d7444ea10152e07e95972bb40b834a13bdd6de1110c"},
{file = "rapidfuzz-3.6.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ca3dfcf74f2b6962f411c33dd95b0adf3901266e770da6281bc96bb5a8b20de9"},
{file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bcc957c0a8bde8007f1a8a413a632a1a409890f31f73fe764ef4eac55f59ca87"},
{file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:692c9a50bea7a8537442834f9bc6b7d29d8729a5b6379df17c31b6ab4df948c2"},
{file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76c23ceaea27e790ddd35ef88b84cf9d721806ca366199a76fd47cfc0457a81b"},
{file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2b155e67fff215c09f130555002e42f7517d0ea72cbd58050abb83cb7c880cec"},
{file = "rapidfuzz-3.6.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3028ee8ecc48250607fa8a0adce37b56275ec3b1acaccd84aee1f68487c8557b"},
{file = "rapidfuzz-3.6.1.tar.gz", hash = "sha256:35660bee3ce1204872574fa041c7ad7ec5175b3053a4cb6e181463fc07013de7"},
]
[package.extras]
full = ["numpy"]
[[package]]
name = "rawpy"
version = "0.19.0"
@ -5257,6 +5602,24 @@ requests = ">=2.0.0"
[package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
[[package]]
name = "rich"
version = "13.7.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
{file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "rpds-py"
version = "0.15.2"
@ -5502,6 +5865,25 @@ files = [
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
]
[[package]]
name = "soundcloud-v2"
version = "1.3.1"
description = "Python wrapper for the v2 SoundCloud API"
optional = false
python-versions = ">=3.6"
files = [
{file = "soundcloud-v2-1.3.1.tar.gz", hash = "sha256:9a9c12aa22e71566e2ca6015267cabc1856afd79fa458f0fc43c44872c184741"},
{file = "soundcloud_v2-1.3.1-py3-none-any.whl", hash = "sha256:42023db3f488514418cb0e01a173f0350ad915371335d49c4f01aba564c1ec5c"},
]
[package.dependencies]
dacite = "*"
python-dateutil = ">=2.8.2"
requests = "*"
[package.extras]
test = ["coveralls", "pytest", "pytest-dotenv"]
[[package]]
name = "soupsieve"
version = "2.5"
@ -5688,6 +6070,37 @@ Sphinx = ">=5"
lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"]
[[package]]
name = "spotdl"
version = "4.2.4"
description = "Download your Spotify playlists and songs along with album art and metadata"
optional = false
python-versions = ">=3.8,<3.13"
files = [
{file = "spotdl-4.2.4-py3-none-any.whl", hash = "sha256:1f6971bbfdecd44e3a40820518dfb75b9e0fa48741302721faa39b2844e49626"},
{file = "spotdl-4.2.4.tar.gz", hash = "sha256:642a2ebc719448ea1f6a22b4b5cc520d3663d7c6992dd7f5c4615b8f3aac6df0"},
]
[package.dependencies]
beautifulsoup4 = ">=4.12.2,<5.0.0"
fastapi = ">=0.103.0,<0.104.0"
mutagen = ">=1.47.0,<2.0.0"
platformdirs = ">=3.11.0,<4.0.0"
pydantic = ">=2.5.2,<3.0.0"
pykakasi = ">=2.2.1,<3.0.0"
python-slugify = {version = ">=8.0.1,<9.0.0", extras = ["unidecode"]}
pytube = ">=15.0.0,<16.0.0"
rapidfuzz = ">=3.5.2,<4.0.0"
requests = ">=2.31.0,<3.0.0"
rich = ">=13.7.0,<14.0.0"
setuptools = ">=69.0.2,<70.0.0"
soundcloud-v2 = ">=1.3.1,<2.0.0"
spotipy = ">=2.23.0,<3.0.0"
syncedlyrics = ">=0.7.0,<0.8.0"
uvicorn = ">=0.23.2,<0.24.0"
yt-dlp = ">=2023.11.16,<2024.0.0"
ytmusicapi = ">=1.3.2,<2.0.0"
[[package]]
name = "spotipy"
version = "2.23.0"
@ -5866,6 +6279,22 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-
tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
typing = ["mypy (>=1.4)", "rich", "twisted"]
[[package]]
name = "syncedlyrics"
version = "0.7.0"
description = "Get an LRC format (synchronized) lyrics for your music"
optional = false
python-versions = ">=3.8"
files = [
{file = "syncedlyrics-0.7.0-py3-none-any.whl", hash = "sha256:12b5df0dbf620fa1525a7b1ed01d7fe5fd36106c1dab0c109998482fd9bac31f"},
{file = "syncedlyrics-0.7.0.tar.gz", hash = "sha256:14d73ff0062b2fc89acf810f50c5ad652b1eed4830f4a82e00236ce7a21388ed"},
]
[package.dependencies]
beautifulsoup4 = ">=4.12.2,<5.0.0"
rapidfuzz = ">=3.5.2,<4.0.0"
requests = ">=2.31.0,<3.0.0"
[[package]]
name = "tablib"
version = "3.5.0"
@ -6195,6 +6624,17 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"]
[[package]]
name = "unidecode"
version = "1.3.8"
description = "ASCII transliterations of Unicode text"
optional = false
python-versions = ">=3.5"
files = [
{file = "Unidecode-1.3.8-py3-none-any.whl", hash = "sha256:d130a61ce6696f8148a3bd8fe779c99adeb4b870584eeb9526584e9aa091fd39"},
{file = "Unidecode-1.3.8.tar.gz", hash = "sha256:cfdb349d46ed3873ece4586b96aa75258726e2fa8ec21d6f00a591d98806c2f4"},
]
[[package]]
name = "uritemplate"
version = "4.1.1"
@ -6245,13 +6685,13 @@ files = [
[[package]]
name = "uvicorn"
version = "0.24.0.post1"
version = "0.23.2"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"},
{file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"},
{file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"},
{file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
]
[package.dependencies]
@ -6817,6 +7257,20 @@ requests = ">=2.31.0,<3"
urllib3 = ">=1.26.17,<3"
websockets = "*"
[[package]]
name = "ytmusicapi"
version = "1.4.2"
description = "Unofficial API for YouTube Music"
optional = false
python-versions = ">=3.8"
files = [
{file = "ytmusicapi-1.4.2-py3-none-any.whl", hash = "sha256:79e824c1a8a796e3d3dc35fd7fc295a1cb87700a686394d4d7e9b0ba22e5ddb3"},
{file = "ytmusicapi-1.4.2.tar.gz", hash = "sha256:7a0e528eccd304fa9f4d2066860eaf3b50fc1c24b4a56162e17a52a225658582"},
]
[package.dependencies]
requests = ">=2.22"
[[package]]
name = "zope-interface"
version = "6.1"
@ -6872,5 +7326,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "d09c78af4c718b0fe6a1c0190627543ca8a902fc67611d688fb81603b93997fd"
python-versions = ">=3.11,<3.13"
content-hash = "eb20087fe00cc1a298f95eaa4d6ca08ba69ab0ff8babb39182d8fbc21592cb29"

View File

@ -6,10 +6,10 @@ authors = ["Alexandr Karpov <alexandr.d.karpov@gmail.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.11"
python = ">=3.11,<3.13"
pytz = "^2023.3"
psutil = "^5.9.5"
python-slugify = "^7.0.0"
python-slugify = "8.0.1"
pillow = "^10.0.0"
argon2-cffi = "^21.3.0"
whitenoise = "^6.3.0"
@ -103,11 +103,11 @@ pytest-lambda = "^2.2.0"
pgvector = "^0.2.2"
pycld2 = "^0.41"
uuid6 = "^2023.5.2"
uvicorn = "^0.24.0.post1"
uvicorn = "0.23.2"
nltk = "^3.8.1"
pymorphy3 = "^1.2.1"
pymorphy3-dicts-ru = "^2.4.417150.4580142"
fastapi = "^0.104.1"
fastapi = "0.103.0"
pydantic-settings = "^2.0.3"
django-elasticsearch-dsl = "^8.0"
elasticsearch-dsl = "^8.11.0"
@ -116,6 +116,9 @@ deep-translator = "1.4.2"
textract = {git = "https://github.com/Alexander-D-Karpov/textract.git", branch = "master"}
django-otp = "^1.3.0"
qrcode = {extras = ["pil"], version = "^7.4.2"}
spotdl = "^4.2.4"
fuzzywuzzy = "^0.18.0"
python-levenshtein = "^0.23.0"
[build-system]