From 0a49519a4e5b548f2f0d77a1407f8c46044e925b Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Thu, 11 Jan 2024 22:08:11 +0300 Subject: [PATCH] Add music information service and refactor music serializers --- akarpov/music/admin.py | 146 ++++++++- akarpov/music/api/serializers.py | 29 +- akarpov/music/api/views.py | 8 +- akarpov/music/services/db.py | 68 ++++- akarpov/music/services/info.py | 389 ++++++++++++++++++++++++ akarpov/music/services/spotify.py | 41 +-- akarpov/music/services/yandex.py | 104 +------ akarpov/music/services/youtube.py | 6 +- akarpov/music/signals.py | 8 +- akarpov/utils/text.py | 10 + poetry.lock | 490 ++++++++++++++++++++++++++++-- pyproject.toml | 11 +- 12 files changed, 1111 insertions(+), 199 deletions(-) create mode 100644 akarpov/music/services/info.py create mode 100644 akarpov/utils/text.py diff --git a/akarpov/music/admin.py b/akarpov/music/admin.py index 61b2ff4..b2f1585 100644 --- a/akarpov/music/admin.py +++ b/akarpov/music/admin.py @@ -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 = ( + "
{}
" + ) + 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) diff --git a/akarpov/music/api/serializers.py b/akarpov/music/api/serializers.py index 95a13f3..6a1639e 100644 --- a/akarpov/music/api/serializers.py +++ b/akarpov/music/api/serializers.py @@ -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}, diff --git a/akarpov/music/api/views.py b/akarpov/music/api/views.py index 1648f4b..a79a9d1 100644 --- a/akarpov/music/api/views.py +++ b/akarpov/music/api/views.py @@ -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() diff --git a/akarpov/music/services/db.py b/akarpov/music/services/db.py index f6efbb5..6d2728e 100644 --- a/akarpov/music/services/db.py +++ b/akarpov/music/services/db.py @@ -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 diff --git a/akarpov/music/services/info.py b/akarpov/music/services/info.py new file mode 100644 index 0000000..b72e547 --- /dev/null +++ b/akarpov/music/services/info.py @@ -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 diff --git a/akarpov/music/services/spotify.py b/akarpov/music/services/spotify.py index d17a0dc..aa512cc 100644 --- a/akarpov/music/services/spotify.py +++ b/akarpov/music/services/spotify.py @@ -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 diff --git a/akarpov/music/services/yandex.py b/akarpov/music/services/yandex.py index 9ebdef2..537d6c8 100644 --- a/akarpov/music/services/yandex.py +++ b/akarpov/music/services/yandex.py @@ -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) diff --git a/akarpov/music/services/youtube.py b/akarpov/music/services/youtube.py index b95cbb1..229ec01 100644 --- a/akarpov/music/services/youtube.py +++ b/akarpov/music/services/youtube.py @@ -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 diff --git a/akarpov/music/signals.py b/akarpov/music/signals.py index 4489fd2..256e776 100644 --- a/akarpov/music/signals.py +++ b/akarpov/music/signals.py @@ -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) diff --git a/akarpov/utils/text.py b/akarpov/utils/text.py new file mode 100644 index 0000000..f3095e6 --- /dev/null +++ b/akarpov/utils/text.py @@ -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 diff --git a/poetry.lock b/poetry.lock index d65465f..945898e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index b08ae38..a65fa6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,10 +6,10 @@ authors = ["Alexandr Karpov "] 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]