mirror of
https://github.com/Alexander-D-Karpov/akarpov
synced 2024-11-22 01:36:33 +03:00
Add music information service and refactor music serializers
This commit is contained in:
parent
7cb24369ea
commit
0a49519a4e
|
@ -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)
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
389
akarpov/music/services/info.py
Normal file
389
akarpov/music/services/info.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
10
akarpov/utils/text.py
Normal 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
490
poetry.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
|
Loading…
Reference in New Issue
Block a user