diff --git a/.github/changelog.md b/.github/changelog.md index d2993bf1..e0348801 100644 --- a/.github/changelog.md +++ b/.github/changelog.md @@ -2,6 +2,7 @@ - Auth +- New artists/albums Sort by: last played, no. of streams, total stream duration ## Improvements - The context menu now doesn't take forever to open up @@ -12,6 +13,7 @@ - ## Development +- Rewritten the whole DB layer to move stores from memory to the database. ## THE BIG ONE API CHANGES diff --git a/TODO.md b/TODO.md index ef37f2ac..057b7d7d 100644 --- a/TODO.md +++ b/TODO.md @@ -47,4 +47,12 @@ - Remove duplicates on artist page (test with Hanson) - Test foreign keys on delete - Map scrobble info on app start -- Make home page recent items faster! \ No newline at end of file +- Make home page recent items faster! +- Normalize playlists table: + - New table to hold playlist entries +- Normalize similar artists: + - New table to hold similar artist entries + - Create 2 way relationships, such that if an artist A is similar to another B with a certain weight, + then artist B is similar to A with the same weight, unless overwritten. +- Figure out how to update album/artist tables instead of deleting all rows when the app starts +- Move get all filtering and sorting operations to the database since all sort keys are table columns \ No newline at end of file diff --git a/app/api/playlist.py b/app/api/playlist.py index a65c6714..3d61d238 100644 --- a/app/api/playlist.py +++ b/app/api/playlist.py @@ -166,12 +166,7 @@ def add_item_to_playlist(path: PlaylistIDPath, body: AddItemToPlaylistBody): else: trackhashes = [] - # insert_count = PL.add_tracks_to_playlist(int(playlist_id), trackhashes) PlaylistTable.append_to_playlist(int(playlist_id), trackhashes) - - # if insert_count == 0: - # return {"error": "Item already exists in playlist"}, 409 - return {"msg": "Done"}, 200 @@ -211,15 +206,12 @@ def get_playlist(path: PlaylistIDPath, query: GetPlaylistQuery): playlist, tracks = handler() return format_custom_playlist(playlist, tracks) - # playlist = PL.get_playlist_by_id(int(playlistid)) playlist = PlaylistTable.get_by_id(playlistid) if playlist is None: return {"msg": "Playlist not found"}, 404 - # tracks = TrackStore.get_tracks_by_trackhashes(list(playlist.trackhashes)) tracks = TrackTable.get_tracks_by_trackhashes(playlist.trackhashes) - tracks = remove_duplicates(tracks) duration = sum(t.duration for t in tracks) playlist.last_updated = date_string_to_time_passed(playlist.last_updated) @@ -250,7 +242,6 @@ def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm): Update playlist """ playlistid = path.playlistid - # db_playlist = PL.get_playlist_by_id(playlistid) db_playlist = PlaylistTable.get_by_id(playlistid) if db_playlist is None: @@ -270,7 +261,6 @@ def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm): "last_updated": create_new_date(), "name": str(form.name).strip(), "settings": settings, - "trackhashes": json.dumps([]), } if image: @@ -290,7 +280,6 @@ def update_playlist_info(path: PlaylistIDPath, form: UpdatePlaylistForm): p_tuple = (*playlist.values(),) - # PL.update_playlist(playlistid, playlist) PlaylistTable.update_one(playlistid, playlist) playlist = models.Playlist(*p_tuple) @@ -306,8 +295,6 @@ def pin_unpin_playlist(path: PlaylistIDPath): """ Pin playlist. """ - # playlist = PL.get_playlist_by_id(path.playlistid) - playlist = PlaylistTable.get_by_id(path.playlistid) if playlist is None: @@ -320,7 +307,6 @@ def pin_unpin_playlist(path: PlaylistIDPath): except KeyError: settings["pinned"] = True - # PL.update_settings(path.playlistid, settings) PlaylistTable.update_settings(path.playlistid, settings) return {"msg": "Done"}, 200 @@ -330,13 +316,11 @@ def remove_playlist_image(path: PlaylistIDPath): """ Clear playlist image. """ - # playlist = PL.get_playlist_by_id(path.playlistid) playlist = PlaylistTable.get_by_id(path.playlistid) if playlist is None: return {"error": "Playlist not found"}, 404 - # PL.remove_banner(path.playlistid) PlaylistTable.remove_image(path.playlistid) playlist.image = None @@ -355,7 +339,6 @@ def remove_playlist(path: PlaylistIDPath): """ Delete playlist """ - # PL.delete_playlist(path.playlistid) PlaylistTable.remove_one(path.playlistid) return {"msg": "Done"}, 200 @@ -378,7 +361,6 @@ def remove_tracks_from_playlist( # index: int; # } - # PL.remove_tracks_from_playlist(path.playlistid, body.tracks) PlaylistTable.remove_from_playlist(path.playlistid, body.tracks) return {"msg": "Done"}, 200 diff --git a/app/api/search.py b/app/api/search.py index 871c3dcf..f08840f3 100644 --- a/app/api/search.py +++ b/app/api/search.py @@ -2,16 +2,17 @@ Contains all the search routes. """ -from flask import request from unidecode import unidecode -from pydantic import BaseModel, Field +from pydantic import Field from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint from app import models +from app.api.apischemas import GenericLimitSchema +from app.db.libdata import TrackTable from app.lib import searchlib from app.settings import Defaults -from app.store.tracks import TrackStore + tag = Tag(name="Search", description="Search for tracks, albums and artists") api = APIBlueprint("search", __name__, url_prefix="/search", abp_tags=[tag]) @@ -20,30 +21,18 @@ SEARCH_COUNT = 30 """The max amount of items to return per request""" -def query_in_quotes(query: str) -> bool: - """ - Returns True if the query is in quotes - """ - try: - return query.startswith('"') and query.endswith('"') - except AttributeError: - return False - - class Search: def __init__(self, query: str) -> None: self.tracks: list[models.Track] = [] self.query = unidecode(query) - def search_tracks(self, in_quotes=False): + def search_tracks(self): """ Calls :class:`SearchTracks` which returns the tracks that fuzzily match the search terms. Then adds them to the `SearchResults` store. """ - self.tracks = TrackStore.tracks - return searchlib.TopResults().search( - self.query, tracks_only=True, in_quotes=in_quotes - ) + self.tracks = TrackTable.get_all() + return searchlib.TopResults().search(self.query, tracks_only=True) def search_artists(self): """Calls :class:`SearchArtists` which returns the artists that fuzzily match @@ -51,25 +40,23 @@ class Search: """ return searchlib.SearchArtists(self.query)() - def search_albums(self, in_quotes=False): + def search_albums(self): """Calls :class:`SearchAlbums` which returns the albums that fuzzily match the search term. Then adds them to the `SearchResults` store. """ - return searchlib.TopResults().search( - self.query, albums_only=True, in_quotes=in_quotes - ) + return searchlib.TopResults().search(self.query, albums_only=True) def get_top_results( self, limit: int, - in_quotes=False, ): finder = searchlib.TopResults() - return finder.search(self.query, in_quotes=in_quotes, limit=limit) + return finder.search(self.query, limit=limit) -class SearchQuery(BaseModel): +class SearchQuery(GenericLimitSchema): q: str = Field(description="The search query", example=Defaults.API_ARTISTNAME) + start: int = Field(description="The index to start from", default=0, example=0) @api.get("/tracks") @@ -77,11 +64,7 @@ def search_tracks(query: SearchQuery): """ Search tracks """ - - query = query.q - in_quotes = query_in_quotes(query) - - tracks = Search(query).search_tracks(in_quotes) + tracks = Search(query.q).search_tracks() return { "tracks": tracks[:SEARCH_COUNT], @@ -90,15 +73,13 @@ def search_tracks(query: SearchQuery): @api.get("/albums") -def search_albums(query: SearchQuery): +def search_albums( + query: SearchQuery, +): """ Search albums. """ - - query = query.q - in_quotes = query_in_quotes(query) - - albums = Search(query).search_albums(in_quotes) + albums = Search(query.q).search_albums() return { "albums": albums[:SEARCH_COUNT], @@ -111,13 +92,10 @@ def search_artists(query: SearchQuery): """ Search artists. """ - - query = query.q - - if not query: + if not query.q: return {"error": "No query provided"}, 400 - artists = Search(query).search_artists() + artists = Search(query.q).search_artists() return { "artists": artists[:SEARCH_COUNT], @@ -138,15 +116,10 @@ def get_top_results(query: TopResultsQuery): Returns the top results for the given query. """ - limit = query.limit - query = query.q - - in_quotes = query_in_quotes(query) - - if not query: + if not query.q: return {"error": "No query provided"}, 400 - return Search(query).get_top_results(in_quotes=in_quotes, limit=limit) + return Search(query.q).get_top_results(limit=query.limit) class SearchLoadMoreQuery(SearchQuery): @@ -166,17 +139,16 @@ def search_load_more(query: SearchLoadMoreQuery): query = query.q item_type = query.type index = query.index - in_quotes = query_in_quotes(query) if item_type == "tracks": - t = Search(query).search_tracks(in_quotes) + t = Search(query).search_tracks() return { "tracks": t[index : index + SEARCH_COUNT], "more": len(t) > index + SEARCH_COUNT, } elif item_type == "albums": - a = Search(query).search_albums(in_quotes) + a = Search(query).search_albums() return { "albums": a[index : index + SEARCH_COUNT], "more": len(a) > index + SEARCH_COUNT, diff --git a/app/api/stream.py b/app/api/stream.py index 4993539a..db6aada2 100644 --- a/app/api/stream.py +++ b/app/api/stream.py @@ -73,7 +73,7 @@ def send_file_as_chunks(filepath: str, audio_type: str) -> Response: """ # NOTE: +1 makes sure the last byte is included in the range. # NOTE: -1 is used to convert the end index to a 0-based index. - chunk_size = 1024 * 360 # 360 KB + chunk_size = 1024 * 512 # 0.5MB # Get file size file_size = os.path.getsize(filepath) diff --git a/app/db/libdata.py b/app/db/libdata.py index f8750453..ecb0f5de 100644 --- a/app/db/libdata.py +++ b/app/db/libdata.py @@ -14,7 +14,7 @@ from app.models import Album as AlbumModel from app.utils.remove_duplicates import remove_duplicates from app.db import engine -from sqlalchemy import JSON, Boolean, Integer, String, and_, delete, select, update +from sqlalchemy import JSON, Boolean, Integer, String, delete, select, update from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase @@ -32,16 +32,26 @@ class Base(MasterBase, DeclarativeBase): @classmethod def get_all_hashes(cls, create_date: int | None = None): with DbManager() as conn: - if cls.__tablename__ == "track": - stmt = select(TrackTable.trackhash).where(cls.last_mod < create_date) - elif cls.__tablename__ == "album": - stmt = select(AlbumTable.albumhash).where( - cls.created_date < create_date - ) - elif cls.__tablename__ == "artist": - stmt = select(ArtistTable.artisthash).where( - cls.created_date < create_date - ) + if create_date: + if cls.__tablename__ == "track": + stmt = select(TrackTable.trackhash).where( + cls.last_mod < create_date + ) + elif cls.__tablename__ == "album": + stmt = select(AlbumTable.albumhash).where( + cls.created_date < create_date + ) + elif cls.__tablename__ == "artist": + stmt = select(ArtistTable.artisthash).where( + cls.created_date < create_date + ) + else: + if cls.__tablename__ == "track": + stmt = select(TrackTable.trackhash) + elif cls.__tablename__ == "album": + stmt = select(AlbumTable.albumhash) + elif cls.__tablename__ == "artist": + stmt = select(ArtistTable.artisthash) result = conn.execute(stmt) return {row[0] for row in result.fetchall()} @@ -135,7 +145,9 @@ class TrackTable(Base): def get_tracks_by_filepaths(cls, filepaths: list[str]): with DbManager() as conn: result = conn.execute( - select(TrackTable).where(TrackTable.filepath.in_(filepaths)) + select(TrackTable) + .where(TrackTable.filepath.in_(filepaths)) + .order_by(TrackTable.last_mod) ) return tracks_to_dataclasses(result.fetchall()) @@ -155,10 +167,8 @@ class TrackTable(Base): result = conn.execute( select(TrackTable) .where( - and_( - TrackTable.trackhash == hash, - TrackTable.filepath == filepath, - ) + (TrackTable.trackhash == hash) + & (TrackTable.filepath == filepath), ) .order_by(TrackTable.bitrate.desc()) ) @@ -194,9 +204,18 @@ class TrackTable(Base): def get_tracks_by_trackhashes(cls, hashes: Iterable[str], limit: int | None = None): with DbManager() as conn: result = conn.execute( - select(TrackTable).where(TrackTable.trackhash.in_(hashes)).limit(limit) + select(TrackTable) + .where(TrackTable.trackhash.in_(hashes)) + .group_by(TrackTable.trackhash) + .limit(limit) ) - return tracks_to_dataclasses(result.fetchall()) + tracks = tracks_to_dataclasses(result.fetchall()) + + # order the tracks in the same order as the hashes + if type(hashes) == list: + return sorted(tracks, key=lambda x: hashes.index(x.trackhash)) + + return tracks @classmethod def get_recently_added(cls, start: int, limit: int): @@ -212,7 +231,12 @@ class TrackTable(Base): @classmethod def get_recently_played(cls, limit: int): - result = cls.execute(select(cls).order_by(cls.lastplayed.desc()).limit(limit)) + result = cls.execute( + select(cls) + .group_by(cls.trackhash) + .order_by(cls.lastplayed.desc()) + .limit(limit) + ) return tracks_to_dataclasses(result.fetchall()) @classmethod @@ -276,7 +300,13 @@ class AlbumTable(Base): result = conn.execute( select(AlbumTable).where(AlbumTable.albumhash.in_(hashes)).limit(limit) ) - return albums_to_dataclasses(result.fetchall()) + albums = albums_to_dataclasses(result.fetchall()) + + # order the albums in the same order as the hashes + if type(hashes) == list: + return sorted(albums, key=lambda x: hashes.index(x.albumhash)) + + return albums @classmethod def get_albums_by_artisthashes(cls, artisthashes: list[str]): diff --git a/app/db/userdata.py b/app/db/userdata.py index 1e9407f0..7ab3d0db 100644 --- a/app/db/userdata.py +++ b/app/db/userdata.py @@ -1,7 +1,7 @@ import datetime +import enum from shlex import join from typing import Any -from flask_jwt_extended import current_user from sqlalchemy import ( JSON, Boolean, @@ -313,36 +313,41 @@ class PlaylistTable(Base): @classmethod def append_to_playlist(cls, id: int, trackhashes: list[str]): - print("type(trackhashes):", type(trackhashes)) + dbtrackhashes = cls.get_trackhashes(id) + if not dbtrackhashes: + dbtrackhashes = [] + return cls.execute( update(cls) .where((cls.id == id) & (cls.userid == get_current_userid())) - .values(trackhashes=cls.trackhashes + trackhashes), + .values(trackhashes=dbtrackhashes + trackhashes), commit=True, ) @classmethod - def remove_from_playlist(cls, id: int, trackhashes: list[dict[str, Any]]): - # CHECKPOINT: Properly remove tracks from a playlist - # Without messing up the order in case of duplicates - tracks = cls.execute( + def get_trackhashes(cls, id: int) -> list[str]: + result = cls.execute( select(cls.trackhashes).where( (cls.id == id) & (cls.userid == get_current_userid()) ) ) + result = result.fetchone() + if result: + return result[0] - results = tracks.fetchone() - if results: - dbhashes: list[str] = results[0] - + @classmethod + def remove_from_playlist(cls, id: int, trackhashes: list[dict[str, Any]]): + # INFO: Get db trackhashes + dbtrackhashes = cls.get_trackhashes(id) + if dbtrackhashes: for item in trackhashes: - if dbhashes.index(item["trackhash"]) == item["index"]: - dbhashes.remove(item["trackhash"]) + if dbtrackhashes.index(item["trackhash"]) == item["index"]: + dbtrackhashes.remove(item["trackhash"]) return cls.execute( update(cls) .where((cls.id == id) & (cls.userid == get_current_userid())) - .values(trackhashes=dbhashes), + .values(trackhashes=dbtrackhashes), commit=True, ) @@ -381,3 +386,34 @@ class PlaylistTable(Base): .values(image=None), commit=True, ) + + +# class PlaylistTrackTable(Base): +# __tablename__ = "playlisttrack" + +# id: Mapped[int] = mapped_column(primary_key=True) +# trackhash: Mapped[str] = mapped_column(String(), index=True) +# playlistid: Mapped[int] = mapped_column( +# Integer(), ForeignKey("playlist.id", ondelete="cascade") +# ) +# index: Mapped[int] = mapped_column(Integer()) +# userid: Mapped[int] = mapped_column( +# Integer(), ForeignKey("user.id", ondelete="cascade") +# ) + +# @classmethod +# def count_by_playlist() + +# @classmethod +# def insert_many(cls, playlistid: int, trackhashes: list[str]): +# userid = get_current_userid() +# items = [ +# { +# "index": index, +# "userid": userid, +# "trackhash": trackhash, +# "playlistid": playlistid, +# } +# for index, trackhash in enumerate(trackhashes) +# ] +# return cls.execute(insert(cls).values(items), commit=True) diff --git a/app/lib/home/recentlyplayed.py b/app/lib/home/recentlyplayed.py index a22bd61d..a89cb92e 100644 --- a/app/lib/home/recentlyplayed.py +++ b/app/lib/home/recentlyplayed.py @@ -215,14 +215,6 @@ def get_recently_played(limit=7): return items -def get_recently_played_tracks(limit: int): - # records = db.get_recently_played(start=0, limit=limit) - # last_updated = records[0].timestamp - # tracks = TrackStore.get_tracks_by_trackhashes([r.trackhash for r in records]) - # return tracks, last_updated - return TrackTable.get_recently_played(limit) - - def get_recently_played_playlist(limit: int = 100): playlist = Playlist( id="recentlyplayed", @@ -233,12 +225,12 @@ def get_recently_played_playlist(limit: int = 100): trackhashes=[], ) - tracks = get_recently_played_tracks(limit) + tracks = TrackTable.get_recently_played(limit) date = datetime.fromtimestamp(tracks[0].lastplayed) playlist.last_updated = date_string_to_time_passed(create_new_date(date)) images = get_first_4_images(tracks=tracks) playlist.images = images - playlist.set_count(len(tracks)) + playlist.count = len(tracks) return playlist, tracks diff --git a/app/lib/searchlib.py b/app/lib/searchlib.py index a23d67fa..3d6cb1cb 100644 --- a/app/lib/searchlib.py +++ b/app/lib/searchlib.py @@ -8,16 +8,21 @@ from rapidfuzz import process, utils from unidecode import unidecode from app import models -from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb +from app.config import UserConfig +from app.db.libdata import AlbumTable, ArtistTable, TrackTable + +# from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.models.enums import FavType 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_many as serialize_albums from app.serializers.artist import serialize_for_cards from app.serializers.track import serialize_track, serialize_tracks -from app.store.albums import AlbumStore -from app.store.artists import ArtistStore -from app.store.tracks import TrackStore + +# from app.store.albums import AlbumStore +# from app.store.artists import ArtistStore +# from app.store.tracks import TrackStore + from app.utils.remove_duplicates import remove_duplicates # ratio = fuzz.ratio @@ -49,7 +54,8 @@ class Limit: class SearchTracks: def __init__(self, query: str) -> None: self.query = query - self.tracks = TrackStore.tracks + # self.tracks = TrackStore.tracks + self.tracks = TrackTable.get_all() def __call__(self) -> List[models.Track]: """ @@ -72,7 +78,8 @@ class SearchTracks: class SearchArtists: def __init__(self, query: str) -> None: self.query = query - self.artists = ArtistStore.artists + # self.artists = ArtistStore.artists + self.artists = ArtistTable.get_all() def __call__(self): """ @@ -94,7 +101,8 @@ class SearchArtists: class SearchAlbums: def __init__(self, query: str) -> None: self.query = query - self.albums = AlbumStore.albums + # self.albums = AlbumStore.albums + self.albums = AlbumTable.get_all() def __call__(self) -> List[models.Album]: """ @@ -137,7 +145,7 @@ _S2 = TypeVar("_S2") _ResultType = int | float -def get_titles(items: _type): +def get_titles(items: list[_type]): for item in items: if isinstance(item, models.Track): text = item.og_title @@ -161,9 +169,9 @@ class TopResults: def collect_all(): all_items: list[_type] = [] - all_items.extend(ArtistStore.artists) - all_items.extend(TrackStore.tracks) - all_items.extend(AlbumStore.albums) + all_items.extend(ArtistTable.get_all()) + all_items.extend(TrackTable.get_all()) + all_items.extend(AlbumTable.get_all()) return all_items, get_titles(all_items) @@ -186,22 +194,16 @@ class TopResults: return {"type": "track", "item": item} if isinstance(item, models.Album): - tracks = TrackStore.get_tracks_by_albumhash(item.albumhash) + tracks = TrackTable.get_tracks_by_albumhash(item.albumhash) tracks = remove_duplicates(tracks) - item.get_date_from_tracks(tracks) try: item.duration = sum((t.duration for t in tracks)) except AttributeError: item.duration = 0 - item.is_single(tracks) - - if not item.is_single: - item.check_type() - - item.is_favorite = favdb.check_is_favorite( - item.albumhash, fav_type=FavType.album + item.check_type( + tracks, singleTrackAsSingle=UserConfig().showAlbumsAsSingles ) return {"type": "album", "item": item} @@ -210,15 +212,18 @@ class TopResults: track_count = 0 duration = 0 - for track in TrackStore.get_tracks_by_artisthash(item.artisthash): + tracks = TrackTable.get_tracks_by_artisthash(item.artisthash) + tracks = remove_duplicates(tracks) + + for track in tracks: track_count += 1 duration += track.duration - album_count = AlbumStore.count_albums_by_artisthash(item.artisthash) + # album_count = AlbumStore.count_albums_by_artisthash(item.artisthash) - item.set_trackcount(track_count) - item.set_albumcount(album_count) - item.set_duration(duration) + # item.set_trackcount(track_count) + # item.set_albumcount(album_count) + # item.set_duration(duration) return {"type": "artist", "item": item} @@ -230,7 +235,8 @@ class TopResults: tracks.extend(SearchTracks(query)()) if item["type"] == "album": - t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash) + t = TrackTable.get_tracks_by_albumhash(item["item"].albumhash) + # t = TrackStore.get_tracks_by_albumhash(item["item"].albumhash) t.sort(key=lambda x: x.last_mod) # if there are less than the limit, get more tracks @@ -242,7 +248,8 @@ class TopResults: tracks.extend(t) if item["type"] == "artist": - t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash) + # t = TrackStore.get_tracks_by_artisthash(item["item"].artisthash) + t = TrackTable.get_tracks_by_artisthash(item["item"].artisthash) # if there are less than the limit, get more tracks if len(t) < limit: @@ -263,7 +270,8 @@ class TopResults: return SearchAlbums(query)()[:limit] if item["type"] == "artist": - albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) + # albums = AlbumStore.get_albums_by_artisthash(item["item"].artisthash) + albums = AlbumTable.get_albums_by_artisthash(item["item"].artisthash) # if there are less than the limit, get more albums if len(albums) < limit: @@ -279,7 +287,6 @@ class TopResults: limit: int = None, albums_only=False, tracks_only=False, - in_quotes=False, ): items, titles = TopResults.collect_all() results = TopResults.get_results(titles, query) @@ -307,21 +314,13 @@ class TopResults: result = TopResults.map_with_type(result) - if in_quotes: - top_tracks = SearchTracks(query)()[:tracks_limit] - else: - top_tracks = TopResults.get_track_items(result, query, limit=tracks_limit) - + top_tracks = TopResults.get_track_items(result, query, limit=tracks_limit) top_tracks = serialize_tracks(top_tracks) if tracks_only: return top_tracks - if in_quotes: - albums = SearchAlbums(query)()[:albums_limit] - else: - albums = TopResults.get_album_items(result, query, limit=albums_limit) - + albums = TopResults.get_album_items(result, query, limit=albums_limit) albums = serialize_albums(albums) if albums_only: diff --git a/app/models/album.py b/app/models/album.py index dfcb7137..f6262b8a 100644 --- a/app/models/album.py +++ b/app/models/album.py @@ -112,21 +112,26 @@ class Album: Runs all the checks to determine the type of album. """ if self.is_single(tracks, singleTrackAsSingle): - return "single" + self.type = "single" + return if self.is_soundtrack(): - return "soundtrack" + self.type = "soundtrack" + return if self.is_live_album(): - return "live album" + self.type = "live album" + return if self.is_compilation(): - return "compilation" + self.type = "compilation" + return if self.is_ep(): - return "ep" + self.type = "ep" + return - return "album" + self.type = "album" def is_soundtrack(self) -> bool: """ diff --git a/app/models/playlist.py b/app/models/playlist.py index d413010d..16282f67 100644 --- a/app/models/playlist.py +++ b/app/models/playlist.py @@ -5,6 +5,7 @@ from pathlib import Path from typing import Any from app import settings +from app.utils.auth import get_current_userid @dataclass(slots=True) @@ -16,10 +17,10 @@ class Playlist: last_updated: str name: str settings: dict - userid: int - trackhashes: list[str] + trackhashes: list[str] = dataclasses.field(default_factory=list) extra: dict[str, Any] = dataclasses.field(default_factory=dict) + userid: int | None = None thumb: str = "" count: int = 0 duration: int = 0 @@ -28,11 +29,10 @@ class Playlist: pinned: bool = False def __post_init__(self): - # self.trackhashes = json.loads(str(self.trackhashes)) - # self.count = len(self.trackhashes) + self.count = len(self.trackhashes) - # if isinstance(self.settings, str): - # self.settings = dict(json.loads(self.settings)) + if self.userid is None: + self.userid = get_current_userid() self.pinned = self.settings.get("pinned", False) self.has_image = ( @@ -45,12 +45,6 @@ class Playlist: self.image = "None" self.thumb = "None" - # def set_duration(self, duration: int): - # self.duration = duration - - # def set_count(self, count: int): - # self.count = count - def clear_lists(self): """ Removes data from lists to make it lighter for sending diff --git a/app/settings.py b/app/settings.py index d8a7036f..32c36bde 100644 --- a/app/settings.py +++ b/app/settings.py @@ -126,7 +126,7 @@ class Defaults: SM_ARTIST_IMG_SIZE = 128 MD_ARTIST_IMG_SIZE = 256 - HASH_LENGTH = 10 + HASH_LENGTH = 16 API_ALBUMHASH = "bfe300e966" API_ARTISTHASH = "cae59f1fc5" API_TRACKHASH = "0853280a12" diff --git a/app/utils/__init__.py b/app/utils/__init__.py index 7cce8009..c432160c 100644 --- a/app/utils/__init__.py +++ b/app/utils/__init__.py @@ -1,5 +1,5 @@ import locale -from typing import TypeVar +from typing import Iterable, TypeVar T = TypeVar("T") @@ -16,5 +16,5 @@ def format_number(number: float) -> str: -def flatten(list_: list[list[T]]) -> list[T]: +def flatten(list_: Iterable[list[T]]) -> list[T]: return [item for sublist in list_ for item in sublist] diff --git a/app/utils/hashing.py b/app/utils/hashing.py index 98a883ea..7671012e 100644 --- a/app/utils/hashing.py +++ b/app/utils/hashing.py @@ -1,4 +1,5 @@ import hashlib +import xxhash from unidecode import unidecode @@ -32,11 +33,12 @@ def create_hash(*args: str, decode=False, limit=10) -> str: str_ = unidecode(str_) str_ = str_.encode("utf-8") - str_ = hashlib.sha1(str_).hexdigest() + return xxhash.xxh3_64(str_).hexdigest() + # str_ = hashlib.sha1(str_).hexdigest() # INFO: Return first 5 + last 5 characters - return ( - str_[: limit // 2] + str_[-limit // 2 :] - if limit % 2 == 0 - else str_[: limit // 2] + str_[-limit // 2 - 1 :] - ) + # return ( + # str_[: limit // 2] + str_[-limit // 2 :] + # if limit % 2 == 0 + # else str_[: limit // 2] + str_[-limit // 2 - 1 :] + # ) diff --git a/poetry.lock b/poetry.lock index 10c11ade..06d77703 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2542,6 +2542,123 @@ files = [ {file = "wrapt-1.15.0.tar.gz", hash = "sha256:d06730c6aed78cee4126234cf2d071e01b44b915e725a6cb439a879ec9754a3a"}, ] +[[package]] +name = "xxhash" +version = "3.4.1" +description = "Python binding for xxHash" +optional = false +python-versions = ">=3.7" +files = [ + {file = "xxhash-3.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:91dbfa55346ad3e18e738742236554531a621042e419b70ad8f3c1d9c7a16e7f"}, + {file = "xxhash-3.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:665a65c2a48a72068fcc4d21721510df5f51f1142541c890491afc80451636d2"}, + {file = "xxhash-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb11628470a6004dc71a09fe90c2f459ff03d611376c1debeec2d648f44cb693"}, + {file = "xxhash-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bef2a7dc7b4f4beb45a1edbba9b9194c60a43a89598a87f1a0226d183764189"}, + {file = "xxhash-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c0f7b2d547d72c7eda7aa817acf8791f0146b12b9eba1d4432c531fb0352228"}, + {file = "xxhash-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00f2fdef6b41c9db3d2fc0e7f94cb3db86693e5c45d6de09625caad9a469635b"}, + {file = "xxhash-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23cfd9ca09acaf07a43e5a695143d9a21bf00f5b49b15c07d5388cadf1f9ce11"}, + {file = "xxhash-3.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:6a9ff50a3cf88355ca4731682c168049af1ca222d1d2925ef7119c1a78e95b3b"}, + {file = "xxhash-3.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f1d7c69a1e9ca5faa75546fdd267f214f63f52f12692f9b3a2f6467c9e67d5e7"}, + {file = "xxhash-3.4.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:672b273040d5d5a6864a36287f3514efcd1d4b1b6a7480f294c4b1d1ee1b8de0"}, + {file = "xxhash-3.4.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:4178f78d70e88f1c4a89ff1ffe9f43147185930bb962ee3979dba15f2b1cc799"}, + {file = "xxhash-3.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9804b9eb254d4b8cc83ab5a2002128f7d631dd427aa873c8727dba7f1f0d1c2b"}, + {file = "xxhash-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c09c49473212d9c87261d22c74370457cfff5db2ddfc7fd1e35c80c31a8c14ce"}, + {file = "xxhash-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ebbb1616435b4a194ce3466d7247df23499475c7ed4eb2681a1fa42ff766aff6"}, + {file = "xxhash-3.4.1-cp310-cp310-win_arm64.whl", hash = "sha256:25dc66be3db54f8a2d136f695b00cfe88018e59ccff0f3b8f545869f376a8a46"}, + {file = "xxhash-3.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:58c49083801885273e262c0f5bbeac23e520564b8357fbb18fb94ff09d3d3ea5"}, + {file = "xxhash-3.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b526015a973bfbe81e804a586b703f163861da36d186627e27524f5427b0d520"}, + {file = "xxhash-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:36ad4457644c91a966f6fe137d7467636bdc51a6ce10a1d04f365c70d6a16d7e"}, + {file = "xxhash-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:248d3e83d119770f96003271fe41e049dd4ae52da2feb8f832b7a20e791d2920"}, + {file = "xxhash-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2070b6d5bbef5ee031666cf21d4953c16e92c2f8a24a94b5c240f8995ba3b1d0"}, + {file = "xxhash-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2746035f518f0410915e247877f7df43ef3372bf36cfa52cc4bc33e85242641"}, + {file = "xxhash-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a8ba6181514681c2591840d5632fcf7356ab287d4aff1c8dea20f3c78097088"}, + {file = "xxhash-3.4.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0aac5010869240e95f740de43cd6a05eae180c59edd182ad93bf12ee289484fa"}, + {file = "xxhash-3.4.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4cb11d8debab1626181633d184b2372aaa09825bde709bf927704ed72765bed1"}, + {file = "xxhash-3.4.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b29728cff2c12f3d9f1d940528ee83918d803c0567866e062683f300d1d2eff3"}, + {file = "xxhash-3.4.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:a15cbf3a9c40672523bdb6ea97ff74b443406ba0ab9bca10ceccd9546414bd84"}, + {file = "xxhash-3.4.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6e66df260fed01ed8ea790c2913271641c58481e807790d9fca8bfd5a3c13844"}, + {file = "xxhash-3.4.1-cp311-cp311-win32.whl", hash = "sha256:e867f68a8f381ea12858e6d67378c05359d3a53a888913b5f7d35fbf68939d5f"}, + {file = "xxhash-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:200a5a3ad9c7c0c02ed1484a1d838b63edcf92ff538770ea07456a3732c577f4"}, + {file = "xxhash-3.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:1d03f1c0d16d24ea032e99f61c552cb2b77d502e545187338bea461fde253583"}, + {file = "xxhash-3.4.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c4bbba9b182697a52bc0c9f8ec0ba1acb914b4937cd4a877ad78a3b3eeabefb3"}, + {file = "xxhash-3.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9fd28a9da300e64e434cfc96567a8387d9a96e824a9be1452a1e7248b7763b78"}, + {file = "xxhash-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6066d88c9329ab230e18998daec53d819daeee99d003955c8db6fc4971b45ca3"}, + {file = "xxhash-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93805bc3233ad89abf51772f2ed3355097a5dc74e6080de19706fc447da99cd3"}, + {file = "xxhash-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64da57d5ed586ebb2ecdde1e997fa37c27fe32fe61a656b77fabbc58e6fbff6e"}, + {file = "xxhash-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97322e9a7440bf3c9805cbaac090358b43f650516486746f7fa482672593df"}, + {file = "xxhash-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbe750d512982ee7d831838a5dee9e9848f3fb440e4734cca3f298228cc957a6"}, + {file = "xxhash-3.4.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fd79d4087727daf4d5b8afe594b37d611ab95dc8e29fe1a7517320794837eb7d"}, + {file = "xxhash-3.4.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:743612da4071ff9aa4d055f3f111ae5247342931dedb955268954ef7201a71ff"}, + {file = "xxhash-3.4.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b41edaf05734092f24f48c0958b3c6cbaaa5b7e024880692078c6b1f8247e2fc"}, + {file = "xxhash-3.4.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:a90356ead70d715fe64c30cd0969072de1860e56b78adf7c69d954b43e29d9fa"}, + {file = "xxhash-3.4.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ac56eebb364e44c85e1d9e9cc5f6031d78a34f0092fea7fc80478139369a8b4a"}, + {file = "xxhash-3.4.1-cp312-cp312-win32.whl", hash = "sha256:911035345932a153c427107397c1518f8ce456f93c618dd1c5b54ebb22e73747"}, + {file = "xxhash-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:f31ce76489f8601cc7b8713201ce94b4bd7b7ce90ba3353dccce7e9e1fee71fa"}, + {file = "xxhash-3.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:b5beb1c6a72fdc7584102f42c4d9df232ee018ddf806e8c90906547dfb43b2da"}, + {file = "xxhash-3.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6d42b24d1496deb05dee5a24ed510b16de1d6c866c626c2beb11aebf3be278b9"}, + {file = "xxhash-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b685fab18876b14a8f94813fa2ca80cfb5ab6a85d31d5539b7cd749ce9e3624"}, + {file = "xxhash-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:419ffe34c17ae2df019a4685e8d3934d46b2e0bbe46221ab40b7e04ed9f11137"}, + {file = "xxhash-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e041ce5714f95251a88670c114b748bca3bf80cc72400e9f23e6d0d59cf2681"}, + {file = "xxhash-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc860d887c5cb2f524899fb8338e1bb3d5789f75fac179101920d9afddef284b"}, + {file = "xxhash-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:312eba88ffe0a05e332e3a6f9788b73883752be63f8588a6dc1261a3eaaaf2b2"}, + {file = "xxhash-3.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:e01226b6b6a1ffe4e6bd6d08cfcb3ca708b16f02eb06dd44f3c6e53285f03e4f"}, + {file = "xxhash-3.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:9f3025a0d5d8cf406a9313cd0d5789c77433ba2004b1c75439b67678e5136537"}, + {file = "xxhash-3.4.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:6d3472fd4afef2a567d5f14411d94060099901cd8ce9788b22b8c6f13c606a93"}, + {file = "xxhash-3.4.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:43984c0a92f06cac434ad181f329a1445017c33807b7ae4f033878d860a4b0f2"}, + {file = "xxhash-3.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a55e0506fdb09640a82ec4f44171273eeabf6f371a4ec605633adb2837b5d9d5"}, + {file = "xxhash-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:faec30437919555b039a8bdbaba49c013043e8f76c999670aef146d33e05b3a0"}, + {file = "xxhash-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:c9e1b646af61f1fc7083bb7b40536be944f1ac67ef5e360bca2d73430186971a"}, + {file = "xxhash-3.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:961d948b7b1c1b6c08484bbce3d489cdf153e4122c3dfb07c2039621243d8795"}, + {file = "xxhash-3.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:719a378930504ab159f7b8e20fa2aa1896cde050011af838af7e7e3518dd82de"}, + {file = "xxhash-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74fb5cb9406ccd7c4dd917f16630d2e5e8cbbb02fc2fca4e559b2a47a64f4940"}, + {file = "xxhash-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5dab508ac39e0ab988039bc7f962c6ad021acd81fd29145962b068df4148c476"}, + {file = "xxhash-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c59f3e46e7daf4c589e8e853d700ef6607afa037bfad32c390175da28127e8c"}, + {file = "xxhash-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cc07256eff0795e0f642df74ad096f8c5d23fe66bc138b83970b50fc7f7f6c5"}, + {file = "xxhash-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9f749999ed80f3955a4af0eb18bb43993f04939350b07b8dd2f44edc98ffee9"}, + {file = "xxhash-3.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:7688d7c02149a90a3d46d55b341ab7ad1b4a3f767be2357e211b4e893efbaaf6"}, + {file = "xxhash-3.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a8b4977963926f60b0d4f830941c864bed16aa151206c01ad5c531636da5708e"}, + {file = "xxhash-3.4.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:8106d88da330f6535a58a8195aa463ef5281a9aa23b04af1848ff715c4398fb4"}, + {file = "xxhash-3.4.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:4c76a77dbd169450b61c06fd2d5d436189fc8ab7c1571d39265d4822da16df22"}, + {file = "xxhash-3.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:11f11357c86d83e53719c592021fd524efa9cf024dc7cb1dfb57bbbd0d8713f2"}, + {file = "xxhash-3.4.1-cp38-cp38-win32.whl", hash = "sha256:0c786a6cd74e8765c6809892a0d45886e7c3dc54de4985b4a5eb8b630f3b8e3b"}, + {file = "xxhash-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:aabf37fb8fa27430d50507deeab2ee7b1bcce89910dd10657c38e71fee835594"}, + {file = "xxhash-3.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6127813abc1477f3a83529b6bbcfeddc23162cece76fa69aee8f6a8a97720562"}, + {file = "xxhash-3.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef2e194262f5db16075caea7b3f7f49392242c688412f386d3c7b07c7733a70a"}, + {file = "xxhash-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71be94265b6c6590f0018bbf73759d21a41c6bda20409782d8117e76cd0dfa8b"}, + {file = "xxhash-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10e0a619cdd1c0980e25eb04e30fe96cf8f4324758fa497080af9c21a6de573f"}, + {file = "xxhash-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fa122124d2e3bd36581dd78c0efa5f429f5220313479fb1072858188bc2d5ff1"}, + {file = "xxhash-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17032f5a4fea0a074717fe33477cb5ee723a5f428de7563e75af64bfc1b1e10"}, + {file = "xxhash-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca7783b20e3e4f3f52f093538895863f21d18598f9a48211ad757680c3bd006f"}, + {file = "xxhash-3.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d77d09a1113899fad5f354a1eb4f0a9afcf58cefff51082c8ad643ff890e30cf"}, + {file = "xxhash-3.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:21287bcdd299fdc3328cc0fbbdeaa46838a1c05391264e51ddb38a3f5b09611f"}, + {file = "xxhash-3.4.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:dfd7a6cc483e20b4ad90224aeb589e64ec0f31e5610ab9957ff4314270b2bf31"}, + {file = "xxhash-3.4.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:543c7fcbc02bbb4840ea9915134e14dc3dc15cbd5a30873a7a5bf66039db97ec"}, + {file = "xxhash-3.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fe0a98d990e433013f41827b62be9ab43e3cf18e08b1483fcc343bda0d691182"}, + {file = "xxhash-3.4.1-cp39-cp39-win32.whl", hash = "sha256:b9097af00ebf429cc7c0e7d2fdf28384e4e2e91008130ccda8d5ae653db71e54"}, + {file = "xxhash-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:d699b921af0dcde50ab18be76c0d832f803034d80470703700cb7df0fbec2832"}, + {file = "xxhash-3.4.1-cp39-cp39-win_arm64.whl", hash = "sha256:2be491723405e15cc099ade1280133ccfbf6322d2ef568494fb7d07d280e7eee"}, + {file = "xxhash-3.4.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:431625fad7ab5649368c4849d2b49a83dc711b1f20e1f7f04955aab86cd307bc"}, + {file = "xxhash-3.4.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc6dbd5fc3c9886a9e041848508b7fb65fd82f94cc793253990f81617b61fe49"}, + {file = "xxhash-3.4.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ff8dbd0ec97aec842476cb8ccc3e17dd288cd6ce3c8ef38bff83d6eb927817"}, + {file = "xxhash-3.4.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef73a53fe90558a4096e3256752268a8bdc0322f4692ed928b6cd7ce06ad4fe3"}, + {file = "xxhash-3.4.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:450401f42bbd274b519d3d8dcf3c57166913381a3d2664d6609004685039f9d3"}, + {file = "xxhash-3.4.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a162840cf4de8a7cd8720ff3b4417fbc10001eefdd2d21541a8226bb5556e3bb"}, + {file = "xxhash-3.4.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b736a2a2728ba45017cb67785e03125a79d246462dfa892d023b827007412c52"}, + {file = "xxhash-3.4.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1d0ae4c2e7698adef58710d6e7a32ff518b66b98854b1c68e70eee504ad061d8"}, + {file = "xxhash-3.4.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6322c4291c3ff174dcd104fae41500e75dad12be6f3085d119c2c8a80956c51"}, + {file = "xxhash-3.4.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:dd59ed668801c3fae282f8f4edadf6dc7784db6d18139b584b6d9677ddde1b6b"}, + {file = "xxhash-3.4.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:92693c487e39523a80474b0394645b393f0ae781d8db3474ccdcead0559ccf45"}, + {file = "xxhash-3.4.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4603a0f642a1e8d7f3ba5c4c25509aca6a9c1cc16f85091004a7028607ead663"}, + {file = "xxhash-3.4.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fa45e8cbfbadb40a920fe9ca40c34b393e0b067082d94006f7f64e70c7490a6"}, + {file = "xxhash-3.4.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:595b252943b3552de491ff51e5bb79660f84f033977f88f6ca1605846637b7c6"}, + {file = "xxhash-3.4.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:562d8b8f783c6af969806aaacf95b6c7b776929ae26c0cd941d54644ea7ef51e"}, + {file = "xxhash-3.4.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:41ddeae47cf2828335d8d991f2d2b03b0bdc89289dc64349d712ff8ce59d0647"}, + {file = "xxhash-3.4.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c44d584afdf3c4dbb3277e32321d1a7b01d6071c1992524b6543025fb8f4206f"}, + {file = "xxhash-3.4.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd7bddb3a5b86213cc3f2c61500c16945a1b80ecd572f3078ddbbe68f9dabdfb"}, + {file = "xxhash-3.4.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ecb6c987b62437c2f99c01e97caf8d25660bf541fe79a481d05732e5236719c"}, + {file = "xxhash-3.4.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:696b4e18b7023527d5c50ed0626ac0520edac45a50ec7cf3fc265cd08b1f4c03"}, + {file = "xxhash-3.4.1.tar.gz", hash = "sha256:0379d6cf1ff987cd421609a264ce025e74f346e3e145dd106c0cc2e3ec3f99a9"}, +] + [[package]] name = "zope-event" version = "5.0" @@ -2616,4 +2733,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "80cb2755efc6cec2cb20d50cb8927dee554991741283e70e7a2665e6253b895d" +content-hash = "184c1c56051131473212b74de5719e2629c630555b13cd4cd2de371b3a2fb195" diff --git a/pyproject.toml b/pyproject.toml index 65574f5d..695cc857 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ flask-jwt-extended = "^4.6.0" sqlalchemy = "^2.0.31" memory-profiler = "^0.61.0" sortedcontainers = "^2.4.0" +xxhash = "^3.4.1" [tool.poetry.dev-dependencies] pylint = "^2.15.5"