try: fix search

This commit is contained in:
cwilvx
2025-02-25 20:53:39 +03:00
parent 7daa20e0df
commit 0a1ab72932
11 changed files with 196 additions and 181 deletions
+3 -1
View File
@@ -11,6 +11,7 @@ from flask_openapi3 import APIBlueprint
from app import models from app import models
from app.api.apischemas import GenericLimitSchema from app.api.apischemas import GenericLimitSchema
from app.lib import searchlib from app.lib import searchlib
from app.serializers.artist import serialize_for_cards
from app.settings import Defaults from app.settings import Defaults
from app.store.tracks import TrackStore from app.store.tracks import TrackStore
@@ -61,7 +62,8 @@ class Search:
"""Calls :class:`SearchArtists` which returns the artists that fuzzily match """Calls :class:`SearchArtists` which returns the artists that fuzzily match
the search term. Then adds them to the `SearchResults` store. the search term. Then adds them to the `SearchResults` store.
""" """
return searchlib.SearchArtists(self.query)() artists = searchlib.SearchArtists(self.query)()
return serialize_for_cards(artists)
def search_albums(self): def search_albums(self):
"""Calls :class:`SearchAlbums` which returns the albums that fuzzily match """Calls :class:`SearchAlbums` which returns the albums that fuzzily match
+2 -8
View File
@@ -519,16 +519,10 @@ class LibDataTable(Base):
@classmethod @classmethod
def get_all_colors(cls, type: str) -> Iterable[dict[str, str]]: def get_all_colors(cls, type: str) -> Iterable[dict[str, str]]:
result = cls.execute( result = cls.execute(select(cls).where(cls.itemtype == type))
select(cls.itemhash, cls.color).where(cls.itemtype == type)
)
# return [
# {"itemhash": r[0].replace(type, ""), "color": r[1]}
# for r in result.fetchall()
# ]
for i in next(result).scalars(): for i in next(result).scalars():
yield {"itemhash": i[0].replace(type, ""), "color": i[1]} yield {"itemhash": i.itemhash.replace(type, ""), "color": i.color}
class MixTable(Base): class MixTable(Base):
+1
View File
@@ -113,6 +113,7 @@ class ProcessArtistColors:
) )
record = LibDataTable.find_one(artisthash, "artist") record = LibDataTable.find_one(artisthash, "artist")
print(record)
if (record is not None) and (record.color is not None): if (record is not None) and (record.color is not None):
continue continue
-5
View File
@@ -39,9 +39,4 @@ def get_recently_played(
iterations += 1 iterations += 1
if iterations == max_iterations:
print(
f"Warning: Reached maximum iterations ({max_iterations}) while fetching recently played items"
)
return items return items
+137 -147
View File
@@ -4,7 +4,7 @@ This library contains all the functions related to the search functionality.
from typing import Any, Generator, List, TypeVar from typing import Any, Generator, List, TypeVar
from rapidfuzz import process, utils from rapidfuzz import process, utils, fuzz
from unidecode import unidecode from unidecode import unidecode
from app import models from app import models
@@ -13,11 +13,14 @@ from app.config import UserConfig
# from app.db.libdata import AlbumTable, ArtistTable, TrackTable # from app.db.libdata import AlbumTable, ArtistTable, TrackTable
# from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb # from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb
from app.models.album import Album
from app.models.artist import Artist
from app.models.enums import FavType from app.models.enums import FavType
from app.models.playlist import Playlist
from app.models.track import Track from app.models.track import Track
from app.serializers.album import serialize_for_card as serialize_album from app.serializers.album import serialize_for_card as serialize_album
from app.serializers.album import serialize_for_card_many as serialize_albums from app.serializers.album import serialize_for_card_many as serialize_albums
from app.serializers.artist import serialize_for_cards from app.serializers.artist import serialize_for_card, serialize_for_cards
from app.serializers.track import serialize_track, serialize_tracks from app.serializers.track import serialize_track, serialize_tracks
from app.store.albums import AlbumStore from app.store.albums import AlbumStore
@@ -35,10 +38,10 @@ class Cutoff:
Holds all the default cutoff values. Holds all the default cutoff values.
""" """
tracks: int = 75 tracks: int = 50
albums: int = 75 albums: int = 50
artists: int = 75 artists: int = 50
playlists: int = 75 playlists: int = 50
class Limit: class Limit:
@@ -57,21 +60,28 @@ class SearchTracks:
self.query = query self.query = query
self.tracks = TrackStore.get_flat_list() self.tracks = TrackStore.get_flat_list()
def __call__(self) -> List[models.Track]: def __call__(self, limit: int = Limit.tracks) -> List[models.Track]:
""" """
Gets all songs with a given title. Gets all songs with a given title.
""" """
track_titles = [unidecode(track.og_title).lower() for track in self.tracks] track_titles = [unidecode(track.title).lower() for track in self.tracks]
results = process.extract( results = process.extract(
self.query, self.query,
track_titles, track_titles,
score_cutoff=Cutoff.tracks, score_cutoff=Cutoff.tracks,
limit=Limit.tracks, limit=limit,
processor=utils.default_process, processor=utils.default_process,
scorer=fuzz.WRatio,
) )
tracks = [self.tracks[i[2]] for i in results] tracks: list[Track] = []
for item in results:
track = self.tracks[item[2]]
track._score = item[1]
tracks.append(track)
return remove_duplicates(tracks) return remove_duplicates(tracks)
@@ -80,21 +90,29 @@ class SearchArtists:
self.query = query self.query = query
self.artists = ArtistStore.get_flat_list() self.artists = ArtistStore.get_flat_list()
def __call__(self): def __call__(self, limit: int = Limit.artists):
""" """
Gets all artists with a given name. Gets all artists with a given name.
""" """
artists = [unidecode(a.name).lower() for a in self.artists] choices = [unidecode(a.name).lower() for a in self.artists]
results = process.extract( results = process.extract(
self.query, self.query,
artists, choices,
score_cutoff=Cutoff.artists, score_cutoff=Cutoff.artists,
limit=Limit.artists, limit=limit,
processor=utils.default_process, processor=utils.default_process,
scorer=fuzz.WRatio,
) )
return [self.artists[i[2]] for i in results] artists: list[Artist] = []
for item in results:
artist = self.artists[item[2]]
artist._score = item[1]
artists.append(artist)
return artists
class SearchAlbums: class SearchAlbums:
@@ -102,22 +120,30 @@ class SearchAlbums:
self.query = query self.query = query
self.albums = AlbumStore.get_flat_list() self.albums = AlbumStore.get_flat_list()
def __call__(self) -> List[models.Album]: def __call__(self, limit: int = Limit.albums):
""" """
Gets all albums with a given title. Gets all albums with a given title.
""" """
albums = [unidecode(a.og_title).lower() for a in self.albums] choices = [unidecode(a.title).lower() for a in self.albums]
results = process.extract( results = process.extract(
self.query, self.query,
albums, choices,
score_cutoff=Cutoff.albums, score_cutoff=Cutoff.albums,
limit=Limit.albums, limit=limit,
processor=utils.default_process, processor=utils.default_process,
scorer=fuzz.token_sort_ratio,
) )
return [self.albums[i[2]] for i in results] albums: list[Album] = []
for item in results:
album = self.albums[item[2]]
album._score = item[1]
albums.append(album)
return albums
class SearchPlaylists: class SearchPlaylists:
@@ -125,22 +151,28 @@ class SearchPlaylists:
self.playlists = playlists self.playlists = playlists
self.query = query self.query = query
def __call__(self) -> List[models.Playlist]: def __call__(self, limit: int = Limit.playlists):
playlists = [p.name for p in self.playlists] choices = [p.name for p in self.playlists]
results = process.extract( results = process.extract(
self.query, self.query,
playlists, choices,
score_cutoff=Cutoff.playlists, score_cutoff=Cutoff.playlists,
limit=Limit.playlists, limit=limit,
processor=utils.default_process, processor=utils.default_process,
scorer=fuzz.WRatio,
) )
return [self.playlists[i[2]] for i in results] playlists: list[Playlist] = []
for item in results:
playlist = self.playlists[item[2]]
playlist._score = item[1]
playlists.append(playlist)
return playlists
_type = models.Track | models.Album | models.Artist _type = models.Track | models.Album | models.Artist
_S2 = TypeVar("_S2")
_ResultType = int | float
def get_titles(items: list[_type]): def get_titles(items: list[_type]):
@@ -169,108 +201,45 @@ class TopResults:
all_items.extend(ArtistStore.get_flat_list()) all_items.extend(ArtistStore.get_flat_list())
all_items.extend(TrackStore.get_flat_list()) all_items.extend(TrackStore.get_flat_list())
all_items.extend(TrackStore.get_flat_list()) all_items.extend(AlbumStore.get_flat_list())
return all_items, get_titles(all_items) return all_items, get_titles(all_items)
@staticmethod @staticmethod
def get_results(items: Generator[str, Any, None], query: str): def get_track_items(item: Track | Album | Artist, limit=5):
items = list(items)
results = process.extract(
query=query, choices=items, score_cutoff=Cutoff.tracks, limit=1
)
return results
@staticmethod
def map_with_type(item: _type):
"""
Map the results to their respective types.
"""
if isinstance(item, models.Track):
return {"type": "track", "item": item}
if isinstance(item, models.Album):
tracks = TrackStore.get_tracks_by_albumhash(item.albumhash)
tracks = remove_duplicates(tracks)
try:
item.duration = sum((t.duration for t in tracks))
except AttributeError:
item.duration = 0
item.check_type(
tracks, singleTrackAsSingle=UserConfig().showAlbumsAsSingles
)
return {"type": "album", "item": item}
if isinstance(item, models.Artist):
track_count = 0
duration = 0
tracks = TrackStore.get_tracks_by_artisthash(item.artisthash)
tracks = remove_duplicates(tracks)
for track in tracks:
track_count += 1
duration += track.duration
return {"type": "artist", "item": item}
@staticmethod
def get_track_items(item: dict[str, _type], query: str, limit=5):
tracks: list[Track] = [] tracks: list[Track] = []
if item["type"] == "track": # INFO: If the item is a track, return empty list
tracks.extend(SearchTracks(query)()) # to be filled by the results from the top search
if isinstance(item, Track):
return tracks
if item["type"] == "album": # INFO: If the item is an album, get the tracks from the album
t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash) if isinstance(item, Album):
t.sort(key=lambda x: x.last_mod) tracks = TrackStore.get_tracks_by_albumhash(item.albumhash)[:limit]
tracks.sort(key=lambda x: x.playduration, reverse=True)
return tracks
# if there are less than the limit, get more tracks # INFO: If the item is an artist, get the tracks from the artist
if len(t) < limit: if isinstance(item, Artist):
remainder = limit - len(t) tracks = TrackStore.get_tracks_by_artisthash(item.artisthash)[:limit]
more_tracks = SearchTracks(query)() tracks.sort(key=lambda x: x.playduration, reverse=True)
t.extend(more_tracks[:remainder])
tracks.extend(t) return tracks
if item["type"] == "artist":
# t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash)
t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash)
# if there are less than the limit, get more tracks
if len(t) < limit:
remainder = limit - len(t)
more_tracks = SearchTracks(query)()
t.extend(more_tracks[:remainder])
tracks.extend(t)
return tracks[:limit]
@staticmethod @staticmethod
def get_album_items(item: dict[str, _type], query: str, limit=6): def get_album_items(item: Track | Album | Artist, limit=6):
if item["type"] == "track": albums: list[Album] = []
return SearchAlbums(query)()[:limit]
if item["type"] == "album": # INFO: If the item is a track or album, search for albums
return SearchAlbums(query)()[:limit] if isinstance(item, Track) or isinstance(item, Album):
return albums
if item["type"] == "artist": # INFO: If the item is an artist, get the albums from the artist
# albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) if isinstance(item, Artist):
albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) albums = AlbumStore.get_albums_by_artisthash(item.artisthash)[:limit]
# if there are less than the limit, get more albums return albums
if len(albums) < limit:
remainder = limit - len(albums)
more_albums = SearchAlbums(query)()
albums.extend(more_albums[:remainder])
return albums[:limit]
@staticmethod @staticmethod
def search( def search(
@@ -279,56 +248,77 @@ class TopResults:
albums_only=False, albums_only=False,
tracks_only=False, tracks_only=False,
): ):
items, titles = TopResults.collect_all()
results = TopResults.get_results(titles, query)
tracks_limit = Limit.tracks if tracks_only else 4 tracks_limit = Limit.tracks if tracks_only else 4
albums_limit = Limit.albums if albums_only else limit albums_limit = Limit.albums if albums_only else limit
artists_limit = limit artists_limit = limit
# map results to their respective items # INFO: Individually search all stores as each type has a different scorer
try: tracks = SearchTracks(query)(limit=tracks_limit) if not albums_only else []
result = [items[i[2]] for i in results][0] albums = SearchAlbums(query)(limit=albums_limit)
except IndexError: artists = SearchArtists(query)(limit=artists_limit)
# INFO: Combine all results and sort them by score
all_results = artists + tracks + albums
all_results = sorted(all_results, key=lambda x: int(x._score), reverse=True)
# INFO: Get the top result
top_result = all_results[0]
top_tracks = []
if not albums_only:
top_tracks = TopResults.get_track_items(top_result, limit=tracks_limit)
# INFO: If there are not enough tracks, fill with search results
if len(top_tracks) < tracks_limit:
found_tracks_set = {track.trackhash for track in top_tracks}
for track in tracks:
if track.trackhash not in found_tracks_set:
top_tracks.append(track)
if len(top_tracks) >= tracks_limit:
break
top_tracks = serialize_tracks(top_tracks)
if tracks_only: if tracks_only:
return [] return top_tracks
if albums_only: top_albums = TopResults.get_album_items(top_result, limit=albums_limit)
return []
return { # INFO: If there are not enough albums, fill with search results
"top_result": None, if len(top_albums) < albums_limit:
"tracks": [], found_albums_set = {album.albumhash for album in top_albums}
"artists": [],
"albums": [],
}
result = TopResults.map_with_type(result) for album in albums:
if album.albumhash not in found_albums_set:
top_albums.append(album)
top_tracks = TopResults.get_track_items(result, query, limit=tracks_limit) if len(top_albums) >= albums_limit:
top_tracks = serialize_tracks(top_tracks) break
if tracks_only: top_albums = serialize_albums(top_albums)
return top_tracks
albums = TopResults.get_album_items(result, query, limit=albums_limit)
albums = serialize_albums(albums)
if albums_only: if albums_only:
return albums return top_albums
artists = SearchArtists(query)()[:artists_limit]
artists = serialize_for_cards(artists) artists = serialize_for_cards(artists)
if result["type"] == "track": if isinstance(top_result, Track):
result["item"] = serialize_track(result["item"]) top_result = serialize_track(top_result)
top_result["type"] = "track"
if result["type"] == "album": if isinstance(top_result, Album):
result["item"] = serialize_album(result["item"]) top_result = serialize_album(top_result)
top_result["type"] = "album"
if isinstance(top_result, Artist):
top_result = serialize_for_card(top_result)
top_result["type"] = "artist"
return { return {
"top_result": result, "top_result": top_result,
"tracks": top_tracks, "tracks": top_tracks,
"artists": artists, "artists": artists,
"albums": albums, "albums": top_albums,
} }
+1
View File
@@ -35,6 +35,7 @@ class Album:
id: int = -1 id: int = -1
type: str = "album" type: str = "album"
image: str = "" image: str = ""
_score: float = 0
versions: list[str] = dataclasses.field(default_factory=list) versions: list[str] = dataclasses.field(default_factory=list)
fav_userids: list[int] = dataclasses.field(default_factory=list) fav_userids: list[int] = dataclasses.field(default_factory=list)
weakhash: str = "" weakhash: str = ""
+1
View File
@@ -54,6 +54,7 @@ class Artist:
id: int = -1 id: int = -1
image: str = "" image: str = ""
_score: float = 0
color: str = "" color: str = ""
fav_userids: list[int] = dataclasses.field(default_factory=list) fav_userids: list[int] = dataclasses.field(default_factory=list)
+1 -1
View File
@@ -27,7 +27,7 @@ class Playlist:
has_image: bool = False has_image: bool = False
images: list[dict[str, str]] = dataclasses.field(default_factory=list) images: list[dict[str, str]] = dataclasses.field(default_factory=list)
pinned: bool = False pinned: bool = False
_score: float = 0
def __post_init__(self): def __post_init__(self):
self.count = len(self.trackhashes) self.count = len(self.trackhashes)
+1
View File
@@ -50,6 +50,7 @@ class Track:
_pos: int = 0 _pos: int = 0
_ati: str = "" _ati: str = ""
image: str = "" image: str = ""
_score: float = 0
explicit: bool = False explicit: bool = False
fav_userids: list[int] = field(default_factory=list) fav_userids: list[int] = field(default_factory=list)
+1 -1
View File
@@ -12,7 +12,6 @@ dependencies = [
"requests>=2.27.1", "requests>=2.27.1",
"colorgram.py>=1.2.0", "colorgram.py>=1.2.0",
"tqdm>=4.65.0", "tqdm>=4.65.0",
"rapidfuzz==2.13.7",
"tinytag>=2.0.0", "tinytag>=2.0.0",
"Unidecode>=1.3.6", "Unidecode>=1.3.6",
"psutil>=5.9.4", "psutil>=5.9.4",
@@ -33,6 +32,7 @@ dependencies = [
"schedule>=1.2.2", "schedule>=1.2.2",
"pillow>=11.1.0", "pillow>=11.1.0",
"flask-openapi3==3.0.2", "flask-openapi3==3.0.2",
"rapidfuzz==3.11.0",
] ]
[dependency-groups] [dependency-groups]
Generated
+48 -18
View File
@@ -1004,25 +1004,55 @@ wheels = [
[[package]] [[package]]
name = "rapidfuzz" name = "rapidfuzz"
version = "2.13.7" version = "3.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/e5/2ab8375be6955aff1925b69c41429cbe54e32a67461c0b59f94c9b8b1cc5/rapidfuzz-2.13.7.tar.gz", hash = "sha256:8d3e252d4127c79b4d7c2ae47271636cbaca905c8bb46d80c7930ab906cf4b5c", size = 999972 } sdist = { url = "https://files.pythonhosted.org/packages/a4/aa/25e5a20131369d82c7b8288c99c2c3011ec47a3f5953ccc9cb8145720be5/rapidfuzz-3.11.0.tar.gz", hash = "sha256:a53ca4d3f52f00b393fab9b5913c5bafb9afc27d030c8a1db1283da6917a860f", size = 57983000 }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/96/a6795d11e94af1563730b9363ca44a410a3e187fd42b93a27a8cbd6881dc/rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b34e8c0e492949ecdd5da46a1cfc856a342e2f0389b379b1a45a3cdcd3176a6e", size = 2442563 }, { url = "https://files.pythonhosted.org/packages/40/ac/9ca008834104ad138fbfe2d7ae4443ada55e00c4eb4272d288897e8763b8/rapidfuzz-3.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8724a978f8af7059c5323d523870bf272a097478e1471295511cf58b2642ff83", size = 1955019 },
{ url = "https://files.pythonhosted.org/packages/6f/5b/ba0a8497a2e154d963c60bac2e2ada81c35899e8c1fe732e3de720a5154e/rapidfuzz-2.13.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:875d51b3497439a72e2d76183e1cb5468f3f979ab2ddfc1d1f7dde3b1ecfb42f", size = 1825126 }, { url = "https://files.pythonhosted.org/packages/4c/55/d026c01c9312c9c2a413679052a9bb884743fc5655e59339116d83a2125b/rapidfuzz-3.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b63cb1f2eb371ef20fb155e95efd96e060147bdd4ab9fc400c97325dfee9fe1", size = 1427753 },
{ url = "https://files.pythonhosted.org/packages/38/c3/94522653c741b716ca5e1380ff5b43bbb738f795d624fa67d70ba3e3f64c/rapidfuzz-2.13.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ae33a72336059213996fe4baca4e0e4860913905c2efb7c991eab33b95a98a0a", size = 1091641 }, { url = "https://files.pythonhosted.org/packages/d1/a0/5f3fae81dd1efdf47da19641e321ae84b4f49a5a7b2ab3ff78bd04a0ae7f/rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82497f244aac10b20710448645f347d862364cc4f7d8b9ba14bd66b5ce4dec18", size = 1411472 },
{ url = "https://files.pythonhosted.org/packages/58/dd/29d1790a6c7c7034593dfda7b67a4d6b6a20cfe9e7def8e0cf325d8abc2c/rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5585189b3d90d81ccd62d4f18530d5ac8972021f0aaaa1ffc6af387ff1dce75", size = 1478821 }, { url = "https://files.pythonhosted.org/packages/3c/3f/770b0fca00faf42983fe21fbd379f429dc2600c58d7015f969fb1f73c1db/rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:339607394941801e6e3f6c1ecd413a36e18454e7136ed1161388de674f47f9d9", size = 5614973 },
{ url = "https://files.pythonhosted.org/packages/09/fa/0b5ec7ab4f4641822e2df585bafb2579327b95822c0b26bc5bf8d8ce25cb/rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:42085d4b154a8232767de8296ac39c8af5bccee6b823b0507de35f51c9cbc2d7", size = 2308161 }, { url = "https://files.pythonhosted.org/packages/08/6f/e3df1c41adf27f4b8a95c9de947ed49e7311a676cd05bdd62a17bb1f21ec/rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:84819390a36d6166cec706b9d8f0941f115f700b7faecab5a7e22fc367408bc3", size = 1665667 },
{ url = "https://files.pythonhosted.org/packages/2a/90/8407b7ff902e660c98406bf9cec4adbbd873aa6ccbf2b14228fc7093535d/rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:585206112c294e335d84de5d5f179c0f932837752d7420e3de21db7fdc476278", size = 1780561 }, { url = "https://files.pythonhosted.org/packages/1a/9b/6c91b98dc70270c35913f359c17e30d4185c83663c4721363540f4c03016/rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eea8d9e20632d68f653455265b18c35f90965e26f30d4d92f831899d6682149b", size = 1676166 },
{ url = "https://files.pythonhosted.org/packages/c9/9f/27ed8be4fc73ff0c229a6b859f8365e293a2302ed3053facee4878c1639d/rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f891b98f8bc6c9d521785816085e9657212621e93f223917fb8e32f318b2957e", size = 1762281 }, { url = "https://files.pythonhosted.org/packages/59/9d/eec7a1bfd3566fb17617b41bfb19556c483241d6864eea3c01b88efe5459/rapidfuzz-3.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b659e1e2ea2784a9a397075a7fc395bfa4fe66424042161c4bcaf6e4f637b38", size = 3130890 },
{ url = "https://files.pythonhosted.org/packages/c0/f1/61d389cd067cf1c9494c232e19223f0ba3fa6c5fef9f0496f67b277c8f43/rapidfuzz-2.13.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08590905a95ccfa43f4df353dcc5d28c15d70664299c64abcad8721d89adce4f", size = 2207652 }, { url = "https://files.pythonhosted.org/packages/26/7c/0a4bb5fbb03a362ea3e1409515d3ae641d9bc869c1375d97d8c47e369cc0/rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1315cd2a351144572e31fe3df68340d4b83ddec0af8b2e207cd32930c6acd037", size = 2339850 },
{ url = "https://files.pythonhosted.org/packages/c9/65/e9d9484ebe787ce91b19235155493bb6b985db12fefee817b82c73d18d7d/rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:b5dd713a1734574c2850c566ac4286594bacbc2d60b9170b795bee4b68656625", size = 1799606 }, { url = "https://files.pythonhosted.org/packages/f8/c1/6b839db83caaa47721398b76390a3145202beb108fa433e842879b497439/rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a7743cca45b4684c54407e8638f6d07b910d8d811347b9d42ff21262c7c23245", size = 6941921 },
{ url = "https://files.pythonhosted.org/packages/82/80/6995b618baab256652ec449e3d0a2b10972da5677d7adf78b2c7dae8237f/rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:988f8f6abfba7ee79449f8b50687c174733b079521c3cc121d65ad2d38831846", size = 2483027 }, { url = "https://files.pythonhosted.org/packages/cc/c9/eaac43bb5e44f3594afddbbdb1a28d7bc0bcb69f93ed9a2ef0c949a48fb2/rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:5bb636b0150daa6d3331b738f7c0f8b25eadc47f04a40e5c23c4bfb4c4e20ae3", size = 2717551 },
{ url = "https://files.pythonhosted.org/packages/37/41/6405ce463e70ed8dfc28a762af19df560ea513555a3f0546d4e244b36b9b/rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b3210869161a864f3831635bb13d24f4708c0aa7208ef5baac1ac4d46e9b4208", size = 2026163 }, { url = "https://files.pythonhosted.org/packages/ef/d3/06ca5ee6b7f030f6527ea1e80fe9a4ab3597e86bc783574e3fc2b05a5265/rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:42f4dd264ada7a9aa0805ea0da776dc063533917773cf2df5217f14eb4429eae", size = 3259550 },
{ url = "https://files.pythonhosted.org/packages/31/c7/ff7b2d3fc6a32c4e0b914f11dde85b0e5875faa49fef57c1a09810d595d1/rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f6fe570e20e293eb50491ae14ddeef71a6a7e5f59d7e791393ffa99b13f1f8c2", size = 2048916 }, { url = "https://files.pythonhosted.org/packages/74/d8/094e75ee0424cce329901a0ff98c1821fd5d9dbc11bcdc9a3fddd2a09c4c/rapidfuzz-3.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51f24cb39e64256221e6952f22545b8ce21cacd59c0d3e367225da8fc4b868d8", size = 4173546 },
{ url = "https://files.pythonhosted.org/packages/ed/cc/4730f99586753877eed251bd276f1d95056b9431a14f53735a1bd47336f9/rapidfuzz-2.13.7-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6120f2995f5154057454c5de99d86b4ef3b38397899b5da1265467e8980b2f60", size = 2421895 }, { url = "https://files.pythonhosted.org/packages/d7/81/f263059e3d9f11b076751ac7ef4eba303fa7f11e32155658953f1697c274/rapidfuzz-3.11.0-cp311-cp311-win32.whl", hash = "sha256:aaf391fb6715866bc14681c76dc0308f46877f7c06f61d62cc993b79fc3c4a2a", size = 1842172 },
{ url = "https://files.pythonhosted.org/packages/90/3d/8507db522fbdeb9a802fe92fa980aa5e0bdb10d047d09db3ffc924d564a7/rapidfuzz-2.13.7-cp311-cp311-win32.whl", hash = "sha256:b20141fa6cee041917801de0bab503447196d372d4c7ee9a03721b0a8edf5337", size = 917098 }, { url = "https://files.pythonhosted.org/packages/33/04/dc42c787f02505a4ca0a961172e8353ceee74ea378b795f3e49686e944b7/rapidfuzz-3.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ebadd5b8624d8ad503e505a99b8eb26fe3ea9f8e9c2234e805a27b269e585842", size = 1621122 },
{ url = "https://files.pythonhosted.org/packages/6c/62/643f0cf5430b601f17ea3671ad2d0e0a285b6ff82f2d83eab196091ef8bc/rapidfuzz-2.13.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec55a81ac2b0f41b8d6fb29aad16e55417036c7563bad5568686931aa4ff08f7", size = 1036221 }, { url = "https://files.pythonhosted.org/packages/4e/0f/461e709bd641922a32bc034976963acbb11d8cf0af28b526f3f35ae07975/rapidfuzz-3.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:d895998fec712544c13cfe833890e0226585cf0391dd3948412441d5d68a2b8c", size = 864792 },
{ url = "https://files.pythonhosted.org/packages/c5/54/954ae2dc7dcb53f5f0953379a4a175d9c2f5e393656ab042843e53780d32/rapidfuzz-3.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f382fec4a7891d66fb7163c90754454030bb9200a13f82ee7860b6359f3f2fa8", size = 1938694 },
{ url = "https://files.pythonhosted.org/packages/f9/74/4682d3370821db5374c0f192d1e4123598190cb53d88936016187f80f154/rapidfuzz-3.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dfaefe08af2a928e72344c800dcbaf6508e86a4ed481e28355e8d4b6a6a5230e", size = 1423836 },
{ url = "https://files.pythonhosted.org/packages/e7/78/ce3d72767e186a9deca30dccb5096cfb03ec49e8e3abf2836ab10d1b4f74/rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92ebb7c12f682b5906ed98429f48a3dd80dd0f9721de30c97a01473d1a346576", size = 1393199 },
{ url = "https://files.pythonhosted.org/packages/3c/21/26bdbe846726ff7793789da07e155699cafa3ba3ed3bee86d472b4762121/rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a1b3ebc62d4bcdfdeba110944a25ab40916d5383c5e57e7c4a8dc0b6c17211a", size = 5543400 },
{ url = "https://files.pythonhosted.org/packages/c9/d5/78e922cfbfc67011ecee9f6c2fd630dee68650d23b9ce78316386a3d8c88/rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c6d7fea39cb33e71de86397d38bf7ff1a6273e40367f31d05761662ffda49e4", size = 1642855 },
{ url = "https://files.pythonhosted.org/packages/df/bb/dcf084c03c46968c3fbc52a33f2a725e0b8bb54ed714f0866c7dad747358/rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:99aebef8268f2bc0b445b5640fd3312e080bd17efd3fbae4486b20ac00466308", size = 1669853 },
{ url = "https://files.pythonhosted.org/packages/ec/3a/9aa7a2c5b611e8d465e82c1d5f8278be7335769165f68f3ffc5a169f4a23/rapidfuzz-3.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4469307f464ae3089acf3210b8fc279110d26d10f79e576f385a98f4429f7d97", size = 3129941 },
{ url = "https://files.pythonhosted.org/packages/d3/15/2bbab50a2634b25593e36241ab9629be253b8c6ea28a34ba6b856bfea661/rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:eb97c53112b593f89a90b4f6218635a9d1eea1d7f9521a3b7d24864228bbc0aa", size = 2302199 },
{ url = "https://files.pythonhosted.org/packages/c6/7c/e3ed92b89c657348c41708fe3b856ebc982c4b220b47299bdef8da374b20/rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ef8937dae823b889c0273dfa0f0f6c46a3658ac0d851349c464d1b00e7ff4252", size = 6904702 },
{ url = "https://files.pythonhosted.org/packages/bd/4f/eed77097068bffb692d6389ae19a531c52a896275e9f5c00566207767537/rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d95f9e9f3777b96241d8a00d6377cc9c716981d828b5091082d0fe3a2924b43e", size = 2679287 },
{ url = "https://files.pythonhosted.org/packages/1f/dc/d2d5dcd5b33a5b394485c67aa13674c8345826af8d3ba0702c06ab2f6430/rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:b1d67d67f89e4e013a5295e7523bc34a7a96f2dba5dd812c7c8cb65d113cbf28", size = 3224946 },
{ url = "https://files.pythonhosted.org/packages/8f/af/17c0c29ded64e464e626dd43fc2e3028c1fa929d10e8201fb2aec654e5b3/rapidfuzz-3.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d994cf27e2f874069884d9bddf0864f9b90ad201fcc9cb2f5b82bacc17c8d5f2", size = 4144678 },
{ url = "https://files.pythonhosted.org/packages/66/5d/5dc02c87d9a0e64e0abd728d3255ddce8475e06b6be3f732a460f0a360c9/rapidfuzz-3.11.0-cp312-cp312-win32.whl", hash = "sha256:ba26d87fe7fcb56c4a53b549a9e0e9143f6b0df56d35fe6ad800c902447acd5b", size = 1824882 },
{ url = "https://files.pythonhosted.org/packages/b7/da/a37d532cbefd7242191abf18f438b315bf5c72d742f78414a8ec1b7396cf/rapidfuzz-3.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1f7efdd7b7adb32102c2fa481ad6f11923e2deb191f651274be559d56fc913b", size = 1606419 },
{ url = "https://files.pythonhosted.org/packages/92/d0/1406d6e110aff87303e98f47adc5e76ef2e69d51cdd08b2d463520158cab/rapidfuzz-3.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:ed78c8e94f57b44292c1a0350f580e18d3a3c5c0800e253f1583580c1b417ad2", size = 858655 },
{ url = "https://files.pythonhosted.org/packages/8a/30/984f1013d28b88304386c8e70b5d63db4765c28be8d9ef68d177c9addc77/rapidfuzz-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e60814edd0c9b511b5f377d48b9782b88cfe8be07a98f99973669299c8bb318a", size = 1931354 },
{ url = "https://files.pythonhosted.org/packages/a4/8a/41d4f95c5742a8a47c0e96c02957f72f8c34411cecde87fe371d5e09807e/rapidfuzz-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f28952da055dbfe75828891cd3c9abf0984edc8640573c18b48c14c68ca5e06", size = 1417918 },
{ url = "https://files.pythonhosted.org/packages/e3/26/031ac8366831da6afc5f25462196eab0e0caf9422c83c007307e23a6f010/rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5e8f93bc736020351a6f8e71666e1f486bb8bd5ce8112c443a30c77bfde0eb68", size = 1388327 },
{ url = "https://files.pythonhosted.org/packages/17/1b/927edcd3b540770d3d6d52fe079c6bffdb99e9dfa4b73585bee2a8bd6504/rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76a4a11ba8f678c9e5876a7d465ab86def047a4fcc043617578368755d63a1bc", size = 5513214 },
{ url = "https://files.pythonhosted.org/packages/0d/a2/c1e4f35e7bfbbd97a665f8cd119d8bd4a085f1721366cd76582dc022131b/rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc0e0d41ad8a056a9886bac91ff9d9978e54a244deb61c2972cc76b66752de9c", size = 1638560 },
{ url = "https://files.pythonhosted.org/packages/39/3f/6827972efddb1e357a0b6165ae9e310d7dc5c078af3023893365c212641b/rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e8ea35f2419c7d56b3e75fbde2698766daedb374f20eea28ac9b1f668ef4f74", size = 1667185 },
{ url = "https://files.pythonhosted.org/packages/cc/5d/6902b93e1273e69ea087afd16e7504099bcb8d712a9f69cb649ea05ca7e1/rapidfuzz-3.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd340bbd025302276b5aa221dccfe43040c7babfc32f107c36ad783f2ffd8775", size = 3107466 },
{ url = "https://files.pythonhosted.org/packages/a6/02/bdb2048c9b8edf4cd82c2e8f6a8ed9af0fbdf91810ca2b36d1be6fc996d8/rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:494eef2c68305ab75139034ea25328a04a548d297712d9cf887bf27c158c388b", size = 2302041 },
{ url = "https://files.pythonhosted.org/packages/12/91/0bbe51e3c15c02578487fd10a14692a40677ea974098d8d376bafd627a89/rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5a167344c1d6db06915fb0225592afdc24d8bafaaf02de07d4788ddd37f4bc2f", size = 6899969 },
{ url = "https://files.pythonhosted.org/packages/27/9d/09b85adfd5829f60bd6dbe53ba66dad22f93a281d494a5638b5f20fb6a8a/rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8c7af25bda96ac799378ac8aba54a8ece732835c7b74cfc201b688a87ed11152", size = 2669022 },
{ url = "https://files.pythonhosted.org/packages/cb/07/6fb723963243335c3bf73925914b6998649d642eff550187454d5bb3d077/rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d2a0f7e17f33e7890257367a1662b05fecaf56625f7dbb6446227aaa2b86448b", size = 3229475 },
{ url = "https://files.pythonhosted.org/packages/3a/8e/e9af6da2e235aa29ad2bb0a1fc2472b2949ed8d9ff8fb0f05b4bfbbf7675/rapidfuzz-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d0d26c7172bdb64f86ee0765c5b26ea1dc45c52389175888ec073b9b28f4305", size = 4143861 },
{ url = "https://files.pythonhosted.org/packages/fd/d8/4677e36e958b4d95d039d254d597db9c020896c8130911dc36b136373b87/rapidfuzz-3.11.0-cp313-cp313-win32.whl", hash = "sha256:6ad02bab756751c90fa27f3069d7b12146613061341459abf55f8190d899649f", size = 1822624 },
{ url = "https://files.pythonhosted.org/packages/e8/97/1c782140e688ea2c3337d94516c635c575aa39fe62782fd53ad5d2119df4/rapidfuzz-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:b1472986fd9c5d318399a01a0881f4a0bf4950264131bb8e2deba9df6d8c362b", size = 1604273 },
{ url = "https://files.pythonhosted.org/packages/a6/83/8b713d50bec947e945a79be47f772484307fc876c426fb26c6f369098389/rapidfuzz-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c408f09649cbff8da76f8d3ad878b64ba7f7abdad1471efb293d2c075e80c822", size = 857385 },
] ]
[[package]] [[package]]
@@ -1223,7 +1253,7 @@ requires-dist = [
{ name = "pendulum", specifier = ">=3.0.0" }, { name = "pendulum", specifier = ">=3.0.0" },
{ name = "pillow", specifier = ">=11.1.0" }, { name = "pillow", specifier = ">=11.1.0" },
{ name = "psutil", specifier = ">=5.9.4" }, { name = "psutil", specifier = ">=5.9.4" },
{ name = "rapidfuzz", specifier = "==2.13.7" }, { name = "rapidfuzz", specifier = "==3.11.0" },
{ name = "requests", specifier = ">=2.27.1" }, { name = "requests", specifier = ">=2.27.1" },
{ name = "schedule", specifier = ">=1.2.2" }, { name = "schedule", specifier = ">=1.2.2" },
{ name = "setproctitle", specifier = ">=1.3.2" }, { name = "setproctitle", specifier = ">=1.3.2" },