From 0b8a5e92f55e05df4f59affee088d40cb11cdddb Mon Sep 17 00:00:00 2001 From: cwilvx Date: Sat, 25 May 2024 15:12:44 +0300 Subject: [PATCH] attach favorites to logged in user --- TODO.md | 1 + app/api/album.py | 1 + app/api/artist.py | 1 + app/api/favorites.py | 14 +++++--- app/api/home/__init__.py | 1 + app/db/sqlite/favorite.py | 56 ++++++++++++++++++++----------- app/db/sqlite/queries.py | 4 ++- app/lib/searchlib.py | 1 + app/migrations/__init__.py | 3 +- app/migrations/v1_4_9/__init__.py | 22 ++++++++++++ app/models/track.py | 20 +++++++++-- app/serializers/track.py | 4 +++ app/store/tracks.py | 32 ++++++++++++++---- jsoni/index.py | 6 ++-- 14 files changed, 128 insertions(+), 38 deletions(-) diff --git a/TODO.md b/TODO.md index 60085e3d..9107b03c 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ 1. Playlists 2. Favorites - Package jsoni and publish on PyPi +- Rewrite stores to use dictionaries instead of list pools # DONE - Add recently played playlist diff --git a/app/api/album.py b/app/api/album.py index 75882b18..139e1621 100644 --- a/app/api/album.py +++ b/app/api/album.py @@ -4,6 +4,7 @@ Contains all the album routes. import random +from flask_jwt_extended import current_user from pydantic import Field from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint diff --git a/app/api/artist.py b/app/api/artist.py index a86c01dd..94c8f6ef 100644 --- a/app/api/artist.py +++ b/app/api/artist.py @@ -6,6 +6,7 @@ import math import random from datetime import datetime +from flask_jwt_extended import current_user from flask_openapi3 import APIBlueprint, Tag from pydantic import Field from app.api.apischemas import AlbumLimitSchema, ArtistHashSchema, ArtistLimitSchema, TrackLimitSchema diff --git a/app/api/favorites.py b/app/api/favorites.py index 2dfe9dbe..94fa57b7 100644 --- a/app/api/favorites.py +++ b/app/api/favorites.py @@ -1,5 +1,6 @@ from typing import List, TypeVar +from flask_jwt_extended import current_user from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint from pydantic import BaseModel, Field @@ -10,7 +11,7 @@ from app.settings import Defaults 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.serializers.artist import serialize_for_card as serialize_artist +from app.serializers.artist import serialize_for_card as serialize_artist, serialize_for_cards from app.serializers.album import serialize_for_card, serialize_for_card_many from app.store.albums import AlbumStore @@ -98,7 +99,9 @@ def get_favorite_tracks(query: GenericLimitSchema): Get favorite tracks """ limit = query.limit - tracks = favdb.get_fav_tracks() + userid = current_user['id'] + + tracks = favdb.get_fav_tracks(userid) trackhashes = [t[1] for t in tracks] trackhashes.reverse() src_tracks = sorted(TrackStore.tracks, key=lambda x: x.trackhash) @@ -118,6 +121,7 @@ def get_favorite_artists(query: GenericLimitSchema): Get favorite artists """ limit = query.limit + artists = favdb.get_fav_artists() artisthashes = [a[1] for a in artists] artisthashes.reverse() @@ -266,9 +270,9 @@ def get_all_favorites(query: GetAllFavoritesQuery): return { "recents": recents[:album_limit], - "tracks": tracks[:track_limit], - "albums": albums[:album_limit], - "artists": artists[:artist_limit], + "tracks": serialize_tracks(tracks[:track_limit]), + "albums": serialize_for_card_many(albums[:album_limit]), + "artists": serialize_for_cards(artists[:artist_limit]), "count": count, } diff --git a/app/api/home/__init__.py b/app/api/home/__init__.py index 5fcfd744..c06a37bb 100644 --- a/app/api/home/__init__.py +++ b/app/api/home/__init__.py @@ -1,3 +1,4 @@ +from flask_jwt_extended import current_user from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint diff --git a/app/db/sqlite/favorite.py b/app/db/sqlite/favorite.py index 162400b3..dfff2872 100644 --- a/app/db/sqlite/favorite.py +++ b/app/db/sqlite/favorite.py @@ -1,4 +1,6 @@ from datetime import datetime + +from flask_jwt_extended import current_user from app.models import FavType from .utils import SQLiteManager @@ -11,12 +13,14 @@ class SQLiteFavoriteMethods: """ Checks if an item is favorited. """ - sql = """SELECT * FROM favorites WHERE hash = ? AND type = ?""" + userid = current_user["id"] + + sql = """SELECT * FROM favorites WHERE hash = ? AND type = ? AND userid = ?""" with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (itemhash, fav_type)) - items = cur.fetchall() + cur.execute(sql, (itemhash, fav_type, userid)) + item = cur.fetchone() cur.close() - return len(items) > 0 + return item is not None @classmethod def insert_one_favorite(cls, fav_type: str, fav_hash: str): @@ -27,10 +31,11 @@ class SQLiteFavoriteMethods: if cls.check_is_favorite(fav_hash, fav_type): return - sql = """INSERT INTO favorites(type, hash, timestamp) VALUES(?,?,?)""" - current_timestamp = datetime.now().timestamp() + sql = """INSERT INTO favorites(type, hash, timestamp, userid) VALUES(?,?,?,?)""" + current_timestamp = int(datetime.now().timestamp()) with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (fav_type, fav_hash, current_timestamp)) + userid = current_user["id"] + cur.execute(sql, (fav_type, fav_hash, current_timestamp, userid)) cur.close() @classmethod @@ -38,55 +43,67 @@ class SQLiteFavoriteMethods: """ Returns a list of all favorites. """ - sql = """SELECT * FROM favorites""" + sql = """SELECT * FROM favorites WHERE userid = ?""" with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql) + userid = current_user["id"] + cur.execute(sql, (userid,)) favs = cur.fetchall() cur.close() return [fav for fav in favs if fav[1] != ""] @classmethod - def get_favorites(cls, fav_type: str) -> list[tuple]: + def get_favorites(cls, fav_type: str, userid: int = None) -> list[tuple]: """ Returns a list of favorite tracks. + + If userid is None, all favorites are returned. """ sql = """SELECT * FROM favorites WHERE type = ?""" + params = (fav_type,) + + if not userid: + sql += " AND userid = ?" + params = (fav_type, userid) + with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (fav_type,)) + cur.execute(sql, params) all_favs = cur.fetchall() cur.close() return all_favs @classmethod - def get_fav_tracks(cls) -> list[tuple]: + def get_fav_tracks(cls, userid: int = None) -> list[tuple]: """ Returns a list of favorite tracks. """ - return cls.get_favorites(FavType.track) + return cls.get_favorites(FavType.track, userid) @classmethod def get_fav_albums(cls) -> list[tuple]: """ Returns a list of favorite albums. """ - return cls.get_favorites(FavType.album) + userid = current_user["id"] + return cls.get_favorites(FavType.album, userid) @classmethod def get_fav_artists(cls) -> list[tuple]: """ Returns a list of favorite artists. """ - return cls.get_favorites(FavType.artist) + userid = current_user["id"] + return cls.get_favorites(FavType.artist, userid) @classmethod def delete_favorite(cls, fav_type: str, fav_hash: str): """ Deletes a favorite from the database. """ - sql = """DELETE FROM favorites WHERE hash = ? AND type = ?""" + sql = """DELETE FROM favorites WHERE hash = ? AND type = ? AND userid = ?""" with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (fav_hash, fav_type)) + userid = current_user["id"] + cur.execute(sql, (fav_hash, fav_type, userid)) cur.close() @classmethod @@ -94,10 +111,11 @@ class SQLiteFavoriteMethods: """ Returns the number of favorite tracks. """ - sql = """SELECT COUNT(*) FROM favorites WHERE type = ?""" + sql = """SELECT COUNT(*) FROM favorites WHERE type = ? AND userid = ?""" with SQLiteManager(userdata_db=True) as cur: - cur.execute(sql, (FavType.track,)) + userid = current_user["id"] + cur.execute(sql, (FavType.track, userid)) count = cur.fetchone()[0] cur.close() return count diff --git a/app/db/sqlite/queries.py b/app/db/sqlite/queries.py index cf084d6a..3bd40b8f 100644 --- a/app/db/sqlite/queries.py +++ b/app/db/sqlite/queries.py @@ -16,7 +16,9 @@ CREATE TABLE IF NOT EXISTS favorites ( id integer PRIMARY KEY, hash text not null, type text not null, - timestamp integer not null default 0 + timestamp integer not null default 0, + userid integer not null, + foreign key (userid) references users(id) on delete cascade ); CREATE TABLE IF NOT EXISTS settings ( diff --git a/app/lib/searchlib.py b/app/lib/searchlib.py index 4b1b1a7b..8bca6420 100644 --- a/app/lib/searchlib.py +++ b/app/lib/searchlib.py @@ -1,6 +1,7 @@ """ This library contains all the functions related to the search functionality. """ + from typing import Any, Generator, List, TypeVar from rapidfuzz import process, utils diff --git a/app/migrations/__init__.py b/app/migrations/__init__.py index 2e9f87ae..9e473e92 100644 --- a/app/migrations/__init__.py +++ b/app/migrations/__init__.py @@ -58,7 +58,8 @@ def apply_migrations(): try: migration.migrate() log.info("Applied migration: %s", migration.__name__) - except: + except Exception as e: log.error("Failed to run migration: %s", migration.__name__) + log.error(e) MigrationManager.set_index(len(all_migrations)) diff --git a/app/migrations/v1_4_9/__init__.py b/app/migrations/v1_4_9/__init__.py index 712be3c3..17720054 100644 --- a/app/migrations/v1_4_9/__init__.py +++ b/app/migrations/v1_4_9/__init__.py @@ -73,3 +73,25 @@ class _3MoveScrobbleToUserId1(Migration): with SQLiteManager(userdata_db=True) as cur: cur.execute("UPDATE track_logger SET userid = 1 WHERE userid = 0") cur.close() + + +class _4AddUserIdToFavoritesTable(Migration): + """ + Adds a userid column to the favorites table. + """ + + @staticmethod + def migrate(): + # check if userid column exists + exists_sql = "select count(*) from pragma_table_info('favorites') where name = 'userid'" + sql = "ALTER TABLE favorites ADD userid INTEGER NOT NULL DEFAULT 1 REFERENCES users(id) ON DELETE CASCADE" + + with SQLiteManager(userdata_db=True) as cur: + data = cur.execute(exists_sql) + data = data.fetchone() + + if data[0] == 1: + return# INFO: column already exists + + cur.executescript(sql) + \ No newline at end of file diff --git a/app/models/track.py b/app/models/track.py index e15fc37f..ba928cc5 100644 --- a/app/models/track.py +++ b/app/models/track.py @@ -1,7 +1,10 @@ -from dataclasses import dataclass +from dataclasses import dataclass, field import os from pathlib import Path +from flask_jwt_extended import current_user + + from app.settings import SessionVarKeys, get_flag from app.utils.hashing import create_hash from app.utils.parsers import ( @@ -40,11 +43,22 @@ class Track: image: str = "" artist_hashes: str = "" - is_favorite: bool = False + + fav_userids: list = field(default_factory=list) + """ + A string of user ids separated by commas. + """ + # is_favorite: bool = False + + @property + def is_favorite(self): + return current_user['id'] in self.fav_userids # temporary attributes _pos: int = 0 # for sorting tracks by disc and track number - _ati: str = "" # (album track identifier) for removing duplicates when merging album versions + _ati: str = ( + "" # (album track identifier) for removing duplicates when merging album versions + ) og_title: str = "" og_album: str = "" diff --git a/app/serializers/track.py b/app/serializers/track.py index b812a641..062b5bd4 100644 --- a/app/serializers/track.py +++ b/app/serializers/track.py @@ -5,6 +5,9 @@ from app.models.track import Track def serialize_track(track: Track, to_remove: set = {}, remove_disc=True) -> dict: album_dict = asdict(track) + # is_favorite @property is not included in asdict + album_dict["is_favorite"] = track.is_favorite + props = { "date", "genre", @@ -16,6 +19,7 @@ def serialize_track(track: Track, to_remove: set = {}, remove_disc=True) -> dict "track", "artist_hashes", "created_date", + "fav_userids", }.union(to_remove) if not remove_disc: diff --git a/app/store/tracks.py b/app/store/tracks.py index dd7981a4..6b4d7a35 100644 --- a/app/store/tracks.py +++ b/app/store/tracks.py @@ -1,5 +1,6 @@ # from tqdm import tqdm +from flask_jwt_extended import current_user from app.db.sqlite.favorite import SQLiteFavoriteMethods as favdb from app.db.sqlite.tracks import SQLiteTrackMethods as trackdb from app.models import Track @@ -25,15 +26,32 @@ class TrackStore: cls.tracks = CustomList(trackdb.get_all_tracks()) - fav_hashes = favdb.get_fav_tracks() - fav_hashes = " ".join([t[1] for t in fav_hashes]) + favs = favdb.get_fav_tracks() + + records = dict() + + for fav in favs: + if fav[1] not in records: + # if trackhash not in dict, add it + # and set the value to a set containing the userid + records[fav[1]] = {fav[4]} + + # if trackhash is in dict, add the userid to the set + records[fav[1]].add(fav[4]) for track in cls.tracks: if instance_key != TRACKS_LOAD_KEY: return - if track.trackhash in fav_hashes: - track.is_favorite = True + try: + track.fav_userids = list(records[track.trackhash]) + except KeyError: + track.fav_userids = [] + + # if track.trackhash in fav_hashes: + # fav = [t for t in favs if t["hash"] == track.trackhash][0] + # print(fav) + # track.favorite_data = [i["userid"] for i in fav] print("Done!") @@ -99,7 +117,8 @@ class TrackStore: for track in cls.tracks: if track.trackhash == trackhash: - track.is_favorite = True + if current_user["id"] not in track.fav_userids: + track.fav_userids.append(current_user["id"]) @classmethod def remove_track_from_fav(cls, trackhash: str): @@ -109,7 +128,8 @@ class TrackStore: for track in cls.tracks: if track.trackhash == trackhash: - track.is_favorite = False + if current_user["id"] in track.fav_userids: + track.fav_userids.remove(current_user["id"]) @classmethod def append_track_artists( diff --git a/jsoni/index.py b/jsoni/index.py index ec504aa1..41c9f21c 100644 --- a/jsoni/index.py +++ b/jsoni/index.py @@ -78,9 +78,9 @@ class MyConfig(Jsoni): name: str = "John" # _configpath: str = "notconfig.json" - # @property - # def _configpath(self): - # return "notconfig.json" + @property + def _configpath(self): + return "notconfig.json" config = MyConfig("notconfig.json")