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

View File

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

View File

@ -10,6 +10,9 @@
from pydub import AudioSegment from pydub import AudioSegment
from akarpov.music.models import Album, Author, Song 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( def load_track(
@ -23,6 +26,20 @@ def load_track(
**kwargs, **kwargs,
) -> Song: ) -> Song:
p_name = path.split("/")[-1] 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("['"): if album and type(album) is str and album.startswith("['"):
album = album.replace("['", "").replace("']", "") album = album.replace("['", "").replace("']", "")
@ -81,26 +98,34 @@ def load_track(
) )
if user_id: if user_id:
song.user_id = user_id song.creator = User.objects.get(id=user_id)
if kwargs: if release:
song.meta = kwargs kwargs["release"] = release
new_file_name = ( kwargs = {
str( "explicit": kwargs["explicit"] if "explicit" in kwargs else None,
slugify( "genre": genre,
GoogleTranslator(source="auto", target="en").translate( "lyrics": kwargs["lyrics"] if "lyrics" in kwargs else None,
f"{song.name} {' '.join([x.name for x in authors])}", "track_source": kwargs["track_source"] if "track_source" in kwargs else None,
target_language="en", } | 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: if image_path:
with open(path, "rb") as file, open(image_path, "rb") as image: with open(path, "rb") as file, open(image_path, "rb") as image:
song.image = File(image, name=image_path.split("/")[-1]) song.image = File(image, name=generated_name + ".png")
song.file = File(file, name=new_file_name) song.file = File(file, name=new_file_name)
song.save() song.save()
else: else:
@ -136,9 +161,9 @@ def load_track(
data=f.read(), data=f.read(),
) )
) )
if "release" in kwargs and kwargs["release"]: if release:
tag.tags.add(TORY(text=kwargs["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.tags.add(TCON(text=kwargs["genre"]))
tag.save() tag.save()
@ -148,4 +173,19 @@ def load_track(
if os.path.exists(image_path): if os.path.exists(image_path):
os.remove(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 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 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: if not settings.MUSIC_SPOTIFY_ID or not settings.MUSIC_SPOTIFY_SECRET:
raise ConnectionError("No spotify credentials provided") raise ConnectionError("No spotify credentials provided")
return spotipy.Spotify( return spotipy.Spotify(
auth_manager=SpotifyClientCredentials( auth_manager=SpotifyClientCredentials(
client_id=settings.MUSIC_SPOTIFY_ID, client_id=settings.MUSIC_SPOTIFY_ID,
@ -14,40 +15,6 @@ def login() -> spotipy.Spotify:
) )
def search(name: str, search_type="track"): def search(name: str, session: spotipy.Spotify, search_type="track"):
sp = login() res = session.search(name, type=search_type)
res = sp.search(name, type=search_type)
return res 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 import os
from random import randint from random import randint
from time import sleep
from django.conf import settings from django.conf import settings
from django.core.files import File from yandex_music import Client, Playlist, Track
from yandex_music import Client, Playlist, Search, Track from yandex_music.exceptions import NetworkError, NotFoundError
from yandex_music.exceptions import NotFoundError
from akarpov.music import tasks from akarpov.music import tasks
from akarpov.music.models import Album as AlbumModel from akarpov.music.models import Song, SongInQue
from akarpov.music.models import Author, Song, SongInQue
from akarpov.music.services.db import load_track from akarpov.music.services.db import load_track
@ -18,34 +17,6 @@ def login() -> Client:
return Client(settings.MUSIC_YANDEX_TOKEN).init() 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: def load_file_meta(track: int, user_id: int) -> str:
que = SongInQue.objects.create() que = SongInQue.objects.create()
client = login() client = login()
@ -66,8 +37,12 @@ def load_file_meta(track: int, user_id: int) -> str:
filename = f"_{str(randint(10000, 9999999))}" filename = f"_{str(randint(10000, 9999999))}"
orig_path = f"{settings.MEDIA_ROOT}/{filename}.mp3" orig_path = f"{settings.MEDIA_ROOT}/{filename}.mp3"
album = track.albums[0] 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") img_pth = str(settings.MEDIA_ROOT + f"/_{str(randint(10000, 99999))}.png")
try: try:
@ -110,64 +85,3 @@ def load_playlist(link: str, user_id: int):
tasks.load_ym_file_meta.apply_async( tasks.load_ym_file_meta.apply_async(
kwargs={"track": track.track.id, "user_id": user_id} 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.models import Song
from akarpov.music.services.db import load_track 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 final_filename = None
@ -99,7 +99,7 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
st = chapters[i][1] st = chapters[i][1]
audio = sound[st:] audio = sound[st:]
chapter_path = path.split(".")[0] + chapters[i][2] + ".mp3" 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") audio.export(chapter_path, format="mp3")
r = requests.get(info["album_image"]) r = requests.get(info["album_image"])
img_pth = str( img_pth = str(
@ -136,7 +136,7 @@ def download_from_youtube_link(link: str, user_id: int) -> Song:
else: else:
print(f"[processing] loading {title}") print(f"[processing] loading {title}")
info = get_track_info(title) info = search_all_platforms(title)
r = requests.get(info["album_image"]) r = requests.get(info["album_image"])
img_pth = str( img_pth = str(
settings.MEDIA_ROOT settings.MEDIA_ROOT

View File

@ -4,7 +4,7 @@
from django.dispatch import receiver from django.dispatch import receiver
from akarpov.music.models import Album, Author, PlaylistSong, Song, SongUserRating 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) @receiver(post_delete, sender=Song)
@ -17,13 +17,15 @@ def auto_delete_file_on_delete(sender, instance, **kwargs):
@receiver(post_save, sender=Author) @receiver(post_save, sender=Author)
def author_create(sender, instance, created, **kwargs): def author_create(sender, instance, created, **kwargs):
if created: 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) @receiver(post_save, sender=Album)
def album_create(sender, instance, created, **kwargs): def album_create(sender, instance, created, **kwargs):
if created: 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) @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"] docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
tests = ["pytest", "pytest-cov", "pytest-xdist"] 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]] [[package]]
name = "daphne" name = "daphne"
version = "4.0.0" version = "4.0.0"
@ -1353,6 +1366,23 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, {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]] [[package]]
name = "dill" name = "dill"
version = "0.3.7" version = "0.3.7"
@ -2190,20 +2220,19 @@ python-dateutil = ">=2.4"
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.104.1" version = "0.103.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "fastapi-0.104.1-py3-none-any.whl", hash = "sha256:752dc31160cdbd0436bb93bad51560b57e525cbb1d4bbf6f4904ceee75548241"}, {file = "fastapi-0.103.0-py3-none-any.whl", hash = "sha256:61ab72c6c281205dd0cbaccf503e829a37e0be108d965ac223779a8479243665"},
{file = "fastapi-0.104.1.tar.gz", hash = "sha256:e5e4540a7c5e1dcfbbcf5b903c234feddcdcd881f191977a1c5dfd917487e7ae"}, {file = "fastapi-0.103.0.tar.gz", hash = "sha256:4166732f5ddf61c33e9fa4664f73780872511e0598d4d5434b1816dc1e6d9421"},
] ]
[package.dependencies] [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" 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" starlette = ">=0.27.0,<0.28.0"
typing-extensions = ">=4.8.0" typing-extensions = ">=4.5.0"
[package.extras] [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)"] 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"}, {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]] [[package]]
name = "greenlet" name = "greenlet"
version = "3.0.3" version = "3.0.3"
@ -2882,6 +2925,16 @@ files = [
[package.extras] [package.extras]
colors = ["colorama (>=0.4.6)"] 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]] [[package]]
name = "jedi" name = "jedi"
version = "0.19.1" version = "0.19.1"
@ -3140,6 +3193,126 @@ interegular = ["interegular (>=0.3.1,<0.4.0)"]
nearley = ["js2py"] nearley = ["js2py"]
regex = ["regex"] 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]] [[package]]
name = "livereload" name = "livereload"
version = "2.6.3" 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]"] 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"] 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]] [[package]]
name = "markupsafe" name = "markupsafe"
version = "2.1.3" version = "2.1.3"
@ -3429,6 +3626,17 @@ files = [
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, {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]] [[package]]
name = "msgpack" name = "msgpack"
version = "1.0.7" version = "1.0.7"
@ -3956,13 +4164,13 @@ tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "pa
[[package]] [[package]]
name = "platformdirs" 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\"." description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.7"
files = [ files = [
{file = "platformdirs-4.1.0-py3-none-any.whl", hash = "sha256:11c8f37bcca40db96d8144522d925583bdb7a31f7b0e37e3ed4318400a8e2380"}, {file = "platformdirs-3.11.0-py3-none-any.whl", hash = "sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"},
{file = "platformdirs-4.1.0.tar.gz", hash = "sha256:906d548203468492d432bcb294d4bc2fff751bf84971fbb2c10918cc206ee420"}, {file = "platformdirs-3.11.0.tar.gz", hash = "sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3"},
] ]
[package.extras] [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"] 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)"] 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]] [[package]]
name = "pylint" name = "pylint"
version = "3.0.3" version = "3.0.3"
@ -4869,6 +5097,20 @@ files = [
[package.extras] [package.extras]
cli = ["click (>=5.0)"] 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]] [[package]]
name = "python-magic" name = "python-magic"
version = "0.4.27" version = "0.4.27"
@ -4912,17 +5154,18 @@ XlsxWriter = ">=0.5.7"
[[package]] [[package]]
name = "python-slugify" name = "python-slugify"
version = "7.0.0" version = "8.0.1"
description = "A Python slugify application that also handles Unicode" description = "A Python slugify application that also handles Unicode"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "python-slugify-7.0.0.tar.gz", hash = "sha256:7a0f21a39fa6c1c4bf2e5984c9b9ae944483fd10b54804cb0e23a3ccd4954f0b"}, {file = "python-slugify-8.0.1.tar.gz", hash = "sha256:ce0d46ddb668b3be82f4ed5e503dbc33dd815d83e2eb6824211310d3fb172a27"},
{file = "python_slugify-7.0.0-py2.py3-none-any.whl", hash = "sha256:003aee64f9fd955d111549f96c4b58a3f40b9319383c70fad6277a4974bbf570"}, {file = "python_slugify-8.0.1-py2.py3-none-any.whl", hash = "sha256:70ca6ea68fe63ecc8fa4fcf00ae651fc8a5d02d93dcd12ae6d4fc7ca46c4d395"},
] ]
[package.dependencies] [package.dependencies]
text-unidecode = ">=1.3" text-unidecode = ">=1.3"
Unidecode = {version = ">=1.1.1", optional = true, markers = "extra == \"unidecode\""}
[package.extras] [package.extras]
unidecode = ["Unidecode (>=1.1.1)"] unidecode = ["Unidecode (>=1.1.1)"]
@ -5040,6 +5283,108 @@ maintainer = ["zest.releaser[recommended]"]
pil = ["pillow (>=9.1.0)"] pil = ["pillow (>=9.1.0)"]
test = ["coverage", "pytest"] 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]] [[package]]
name = "rawpy" name = "rawpy"
version = "0.19.0" version = "0.19.0"
@ -5257,6 +5602,24 @@ requests = ">=2.0.0"
[package.extras] [package.extras]
rsa = ["oauthlib[signedtoken] (>=3.0.0)"] 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]] [[package]]
name = "rpds-py" name = "rpds-py"
version = "0.15.2" version = "0.15.2"
@ -5502,6 +5865,25 @@ files = [
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, {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]] [[package]]
name = "soupsieve" name = "soupsieve"
version = "2.5" version = "2.5"
@ -5688,6 +6070,37 @@ Sphinx = ">=5"
lint = ["docutils-stubs", "flake8", "mypy"] lint = ["docutils-stubs", "flake8", "mypy"]
test = ["pytest"] 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]] [[package]]
name = "spotipy" name = "spotipy"
version = "2.23.0" 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"] tests = ["freezegun (>=0.2.8)", "pretend", "pytest (>=6.0)", "pytest-asyncio (>=0.17)", "simplejson"]
typing = ["mypy (>=1.4)", "rich", "twisted"] 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]] [[package]]
name = "tablib" name = "tablib"
version = "3.5.0" version = "3.5.0"
@ -6195,6 +6624,17 @@ tzdata = {version = "*", markers = "platform_system == \"Windows\""}
[package.extras] [package.extras]
devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] 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]] [[package]]
name = "uritemplate" name = "uritemplate"
version = "4.1.1" version = "4.1.1"
@ -6245,13 +6685,13 @@ files = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.24.0.post1" version = "0.23.2"
description = "The lightning-fast ASGI server." description = "The lightning-fast ASGI server."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, {file = "uvicorn-0.23.2-py3-none-any.whl", hash = "sha256:1f9be6558f01239d4fdf22ef8126c39cb1ad0addf76c40e760549d2c2f43ab53"},
{file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, {file = "uvicorn-0.23.2.tar.gz", hash = "sha256:4d3cc12d7727ba72b64d12d3cc7743124074c0a69f7b201512fc50c3e3f1569a"},
] ]
[package.dependencies] [package.dependencies]
@ -6817,6 +7257,20 @@ requests = ">=2.31.0,<3"
urllib3 = ">=1.26.17,<3" urllib3 = ">=1.26.17,<3"
websockets = "*" 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]] [[package]]
name = "zope-interface" name = "zope-interface"
version = "6.1" version = "6.1"
@ -6872,5 +7326,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = "^3.11" python-versions = ">=3.11,<3.13"
content-hash = "d09c78af4c718b0fe6a1c0190627543ca8a902fc67611d688fb81603b93997fd" content-hash = "eb20087fe00cc1a298f95eaa4d6ca08ba69ab0ff8babb39182d8fbc21592cb29"

View File

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