diff --git a/app/api/artist.py b/app/api/artist.py index a1309f1c..eb425f9e 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -183,7 +183,7 @@ def get_all_artist_tracks(path: ArtistHashSchema): """ tracks = TrackStore.get_tracks_by_artisthash(path.artisthash) - return {"tracks": serialize_tracks(tracks)} + return serialize_tracks(tracks) @api.get("//similar") @@ -208,7 +208,7 @@ def get_similar_artists(path: ArtistHashSchema, query: ArtistLimitSchema): if len(similar) > limit: similar = random.sample(similar, limit) - return {"artists": similar[:limit]} + return similar[:limit] # TODO: Rewrite this file using generators where possible diff --git a/app/api/favorites.py b/app/api/favorites.py index e1ec0db4..6add5aa7 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -1,21 +1,26 @@ +from typing import List, TypeVar from flask import Blueprint, request -from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.models import FavType -from app.serializers.album import serialize_for_card, serialize_for_card_many -from app.serializers.artist import serialize_for_card as serialize_artist +from app.utils.bisection import use_bisection +from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.serializers.track import serialize_track, serialize_tracks -from app.utils.bisection import UseBisection +from app.serializers.artist import serialize_for_card as serialize_artist +from app.serializers.album import serialize_for_card, serialize_for_card_many -from app.store.artists import ArtistStore from app.store.albums import AlbumStore from app.store.tracks import TrackStore +from app.store.artists import ArtistStore +from app.utils.dates import timestamp_to_time_passed api = Blueprint("favorite", __name__, url_prefix="/") -def remove_none(items: list): +T = TypeVar("T") + + +def remove_none(items: List[T]) -> List[T]: return [i for i in items if i is not None] @@ -76,7 +81,7 @@ def get_favorite_albums(): src_albums = sorted(AlbumStore.albums, key=lambda x: x.albumhash) - fav_albums = UseBisection(src_albums, "albumhash", albumhashes)() + fav_albums = use_bisection(src_albums, "albumhash", albumhashes) fav_albums = remove_none(fav_albums) if limit == 0: @@ -99,7 +104,7 @@ def get_favorite_tracks(): trackhashes.reverse() src_tracks = sorted(TrackStore.tracks, key=lambda x: x.trackhash) - tracks = UseBisection(src_tracks, "trackhash", trackhashes)() + tracks = use_bisection(src_tracks, "trackhash", trackhashes) tracks = remove_none(tracks) if limit == 0: @@ -123,7 +128,7 @@ def get_favorite_artists(): src_artists = sorted(ArtistStore.artists, key=lambda x: x.artisthash) - artists = UseBisection(src_artists, "artisthash", artisthashes)() + artists = use_bisection(src_artists, "artisthash", artisthashes) artists = remove_none(artists) if limit == 0: @@ -169,6 +174,7 @@ def get_all_favorites(): artist_master_hash = set(a.artisthash for a in ArtistStore.artists) for fav in favs: + # INFO: hash is [1], type is [2], timestamp is [3] hash = fav[1] if fav[2] == FavType.track: tracks.append(hash) if hash in track_master_hash else None @@ -189,9 +195,9 @@ def get_all_favorites(): src_albums = sorted(AlbumStore.albums, key=lambda x: x.albumhash) src_artists = sorted(ArtistStore.artists, key=lambda x: x.artisthash) - tracks = UseBisection(src_tracks, "trackhash", tracks, limit=track_limit)() - albums = UseBisection(src_albums, "albumhash", albums, limit=album_limit)() - artists = UseBisection(src_artists, "artisthash", artists, limit=artist_limit)() + tracks = use_bisection(src_tracks, "trackhash", tracks, limit=track_limit) + albums = use_bisection(src_albums, "albumhash", albums, limit=album_limit) + artists = use_bisection(src_artists, "artisthash", artists, limit=artist_limit) tracks = remove_none(tracks) albums = remove_none(albums) @@ -201,6 +207,7 @@ def get_all_favorites(): # first_n = favs for fav in favs: + # INFO: hash is [1], type is [2], timestamp is [3] if len(recents) >= largest: break @@ -212,6 +219,7 @@ def get_all_favorites(): album = serialize_for_card(album) album["help_text"] = "album" + album["time"] = timestamp_to_time_passed(fav[3]) recents.append( { @@ -228,6 +236,7 @@ def get_all_favorites(): artist = serialize_artist(artist) artist["help_text"] = "artist" + artist["time"] = timestamp_to_time_passed(fav[3]) recents.append( { @@ -244,6 +253,7 @@ def get_all_favorites(): track = serialize_track(track) track["help_text"] = "track" + track["time"] = timestamp_to_time_passed(fav[3]) recents.append({"type": "track", "item": track}) diff --git a/app/db/sqlite/favorite.py b/app/db/sqlite/favorite.py index fb1ef911..162400b3 100644 --- a/app/db/sqlite/favorite.py +++ b/app/db/sqlite/favorite.py @@ -1,3 +1,4 @@ +from datetime import datetime from app.models import FavType from .utils import SQLiteManager @@ -26,9 +27,10 @@ class SQLiteFavoriteMethods: if cls.check_is_favorite(fav_hash, fav_type): return - sql = """INSERT INTO favorites(type, hash) VALUES(?,?)""" + sql = """INSERT INTO favorites(type, hash, timestamp) VALUES(?,?,?)""" + current_timestamp = datetime.now().timestamp() with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (fav_type, fav_hash)) + cur.execute(sql, (fav_type, fav_hash, current_timestamp)) cur.close() @classmethod diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index c286a004..96b8b5b1 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -15,7 +15,8 @@ CREATE TABLE IF NOT EXISTS playlists ( CREATE TABLE IF NOT EXISTS favorites ( id integer PRIMARY KEY, hash text not null, - type text not null + type text not null, + timestamp integer not null default 0 ); CREATE TABLE IF NOT EXISTS settings ( diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index 7bc87b80..8aaf1f5e 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -13,7 +13,7 @@ PS: Fuck that! Do what you want. from app.db.sqlite.migrations import MigrationManager from app.logger import log -from app.migrations import v1_3_0 +from app.migrations import v1_3_0, v1_4_9 from app.migrations.base import Migration migrations: list[list[Migration]] = [ @@ -26,6 +26,9 @@ migrations: list[list[Migration]] = [ v1_3_0.MovePlaylistsAndFavoritesTo10BitHashes, v1_3_0.RemoveAllTracks, v1_3_0.UpdateAppSettingsTable, + ], + [ + v1_4_9.AddTimestampToFavoritesTable ] ] diff --git a/app/migrations/v1_4_9/__init__.py b/app/migrations/v1_4_9/__init__.py new file mode 100644 index 00000000..4ebd0eae --- /dev/null +++ b/app/migrations/v1_4_9/__init__.py @@ -0,0 +1,34 @@ +from app.db.sqlite.utils import SQLiteManager +from app.migrations.base import Migration + + +class AddTimestampToFavoritesTable(Migration): + """ + Adds a timestamp column to the favorites table. + """ + + @staticmethod + def migrate(): + # INFO: add timestamp column with automatic current timestamp + sql = f"ALTER TABLE favorites ADD COLUMN timestamp INTEGER NOT NULL DEFAULT 0" + + # INFO: execute the sql + with SQLiteManager(userdata_db=True) as cur: + cur.execute(sql) + + # INFO: Update the timestamp column with the current timestamp + cur.execute("UPDATE favorites SET timestamp = strftime('%s', 'now')") + cur.close() + + +class MoveHashesToSha1(Migration): + """ + Moves the 10 bit item hashes from sha256 to sha1 which is + faster and more lenient on less powerful devices. + + Thanks to [@tcsenpai](https:github.com/tcsenpai) for the contribution. + """ + pass + + # INFO: Apparentlly, every single table is affected by this migration. + # NOTE: Use generators to avoid memory issues. \ No newline at end of file diff --git a/app/store/artists.py b/app/store/artists.py index f640c169..84bcb242 100644 --- a/app/store/artists.py +++ b/app/store/artists.py @@ -3,7 +3,7 @@ import json from app.db.sqlite.artistcolors import SQLiteArtistMethods as ardb from app.lib.artistlib import get_all_artists from app.models import Artist -from app.utils.bisection import UseBisection +from app.utils.bisection import use_bisection from app.utils.customlist import CustomList from app.utils.progressbar import tqdm @@ -72,7 +72,7 @@ class ArtistStore: """ artists = sorted(cls.artists, key=lambda x: x.artisthash) try: - artist = UseBisection(artists, "artisthash", [artisthash])()[0] + artist = use_bisection(artists, "artisthash", [artisthash])[0] return artist except IndexError: return None @@ -83,7 +83,7 @@ class ArtistStore: Returns artists by their hashes. """ artists = sorted(cls.artists, key=lambda x: x.artisthash) - artists = UseBisection(artists, "artisthash", artisthashes)() + artists = use_bisection(artists, "artisthash", artisthashes) return [a for a in artists if a is not None] @classmethod diff --git a/app/store/tracks.py b/app/store/tracks.py index 7d8625bb..6b56a829 100644 --- a/app/store/tracks.py +++ b/app/store/tracks.py @@ -3,7 +3,7 @@ from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.sqlite.tracks import SQLiteTrackMethods as tdb from app.models import Track -from app.utils.bisection import UseBisection +from app.utils.bisection import use_bisection from app.utils.customlist import CustomList from app.utils.remove_duplicates import remove_duplicates @@ -153,7 +153,7 @@ class TrackStore: Returns all tracks matching the given paths. """ tracks = sorted(cls.tracks, key=lambda x: x.filepath) - tracks = UseBisection(tracks, "filepath", paths)() + tracks = use_bisection(tracks, "filepath", paths) return [track for track in tracks if track is not None] @classmethod diff --git a/app/utils/bisection.py b/app/utils/bisection.py index 2941f10a..ef88e1d6 100644 --- a/app/utils/bisection.py +++ b/app/utils/bisection.py @@ -1,52 +1,45 @@ -from app.models.track import Track +from typing import List, Optional, TypeVar +T = TypeVar("T") -class UseBisection: +def use_bisection( + source: List[T], key: str, queries: List[str], limit: int = -1 +) -> List[Optional[T]]: """ Uses bisection to find a list of items in another list. - returns a list of found items with `None` items being not found - items. + Returns a list of found items with `None` items being not found items. """ - def __init__( - self, source: list, search_from: str, queries: list[str], limit=-1 - ) -> None: - self.source_list = source - self.queries_list = queries - self.attr = search_from - self.limit = limit - - def find(self, query: str): + def find(query: str): left = 0 - right = len(self.source_list) - 1 + right = len(source) - 1 while left <= right: mid = (left + right) // 2 - if self.source_list[mid].__getattribute__(self.attr) == query: - return self.source_list[mid] - elif self.source_list[mid].__getattribute__(self.attr) > query: + if source[mid].__getattribute__(key) == query: + return source[mid] + elif source[mid].__getattribute__(key) > query: right = mid - 1 else: left = mid + 1 return None - def __call__(self): - if len(self.source_list) == 0: - return [] + if len(source) == 0: + return [] - results: list[Track] = [] + results = [] - for query in self.queries_list: - res = self.find(query) + for query in queries: + res = find(query) - if res is None: - continue + if res is None: + continue - results.append(res) + results.append(res) - if self.limit != -1 and len(results) >= self.limit: - break + if limit != -1 and len(results) >= limit: + break - return results + return results