diff --git a/app/api/scrobble/__init__.py b/app/api/scrobble/__init__.py index 4a9133a2..834fe06f 100644 --- a/app/api/scrobble/__init__.py +++ b/app/api/scrobble/__init__.py @@ -1,20 +1,15 @@ -from dataclasses import dataclass from gettext import ngettext -from itertools import groupby -from math import e -from pprint import pprint from flask_openapi3 import Tag from flask_openapi3 import APIBlueprint import pendulum from pydantic import Field, BaseModel from app.api.apischemas import TrackHashSchema from typing import Literal -from datetime import datetime, timedelta -from collections import defaultdict import locale from app.db.userdata import FavoritesTable, ScrobbleTable from app.lib.extras import get_extra_info +from app.lib.recipes.recents import RecentlyPlayed from app.models.album import Album from app.models.stats import StatItem from app.models.track import Track @@ -80,6 +75,9 @@ def log_track(body: LogTrackBody): scrobble_data["extra"] = get_extra_info(body.trackhash, "track") ScrobbleTable.add(scrobble_data) + # NOTE: Update the recently played homepage for this userid + RecentlyPlayed(userid=scrobble_data["userid"]) + # Update play data on the in-memory stores track = trackentry.tracks[0] album = AlbumStore.albummap.get(track.albumhash) diff --git a/app/crons/__init__.py b/app/crons/__init__.py index 41e09ed3..4d648e18 100644 --- a/app/crons/__init__.py +++ b/app/crons/__init__.py @@ -18,7 +18,7 @@ def start_cron_jobs(): RecentlyAdded() # Initialized CRON jobs - # Mixes() + Mixes() TopArtists() TopArtists(duration="week") diff --git a/app/db/userdata.py b/app/db/userdata.py index 8cc0fd54..f529413d 100644 --- a/app/db/userdata.py +++ b/app/db/userdata.py @@ -26,6 +26,7 @@ from app.db.utils import ( plugin_to_dataclasses, similar_artist_to_dataclass, similar_artists_to_dataclass, + tracklog_to_dataclass, tracklog_to_dataclasses, user_to_dataclass, user_to_dataclasses, @@ -319,6 +320,13 @@ class ScrobbleTable(Base): ) return tracklog_to_dataclasses(result.fetchall()) + @classmethod + def get_last_entry(cls, userid: int): + result = cls.execute( + select(cls).where(cls.userid == userid).order_by(cls.timestamp.desc()) + ) + return tracklog_to_dataclass(result.fetchone()) + class PlaylistTable(Base): __tablename__ = "playlist" diff --git a/app/lib/home/recentlyplayed.py b/app/lib/home/recentlyplayed.py index 0476d26b..1797c01c 100644 --- a/app/lib/home/recentlyplayed.py +++ b/app/lib/home/recentlyplayed.py @@ -22,7 +22,14 @@ from app.store.tracks import TrackStore from app.store.artists import ArtistStore -def get_recently_played(limit=7, userid: int | None = None): +def get_recently_played( + limit: int, userid: int | None = None, _entries: list[TrackLog] = [] +): + """ + Get the recently played items for the homepage. + + Pass a list of track log entries to use a subset of the scrobble table. + """ # TODO: Paginate this items = [] added = set() @@ -48,29 +55,12 @@ def get_recently_played(limit=7, userid: int | None = None): if album is None: continue - # album = album_serializer( - # album, - # to_remove={ - # "genres", - # "date", - # "count", - # "duration", - # "albumartists_hashes", - # "og_title", - # }, - # ) item = { "type": "album", "hash": entry.type_src, "timestamp": entry.timestamp, } - # album["help_text"] = "album" - # album["time"] = timestamp_to_time_passed(entry.timestamp) - # { - # "type": "album", - # "item": album, - # } items.append(item) continue @@ -80,10 +70,6 @@ def get_recently_played(limit=7, userid: int | None = None): if artist is None: continue - # artist = serialize_for_card(artist) - # artist["help_text"] = "artist" - # artist["time"] = timestamp_to_time_passed(entry.timestamp) - items.append( { "type": "artist", @@ -108,7 +94,6 @@ def get_recently_played(limit=7, userid: int | None = None): if is_home_dir: folder = os.path.expanduser("~") - # count = FolderStore.count_tracks_containing_paths([folder]) item = { "type": "folder", "hash": entry.type_src, @@ -116,42 +101,12 @@ def get_recently_played(limit=7, userid: int | None = None): } items.append(item) - # { - # "type": "folder", - # "item": { - # "path": folder, - # "count": count[0]["trackcount"], - # "help_text": "folder", - # "time": timestamp_to_time_passed(entry.timestamp), - # }, - # } - continue if entry.type == "playlist": is_custom = entry.type_src in [i["name"] for i in custom_playlists] if is_custom: - # playlist, _ = next( - # i["handler"]() - # for i in custom_playlists - # if i["name"] == entry.type_src - # ) - # playlist.images = [i["image"] for i in playlist.images] - - # playlist = serialize_playlist( - # playlist, to_remove={"settings", "duration"} - # ) - - # playlist["help_text"] = "playlist" - # playlist["time"] = timestamp_to_time_passed(entry.timestamp) - - # items.append( - # { - # "type": "playlist", - # "item": playlist, - # } - # ) items.append( { "type": "playlist", @@ -173,37 +128,10 @@ def get_recently_played(limit=7, userid: int | None = None): } items.append(item) - - # tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes) - # playlist.clear_lists() - - # if not playlist.has_image: - # images = get_first_4_images(tracks) - # images = [i["image"] for i in images] - # playlist.images = images - - # items.append( - # { - # "type": "playlist", - # "item": { - # "help_text": "playlist", - # "time": timestamp_to_time_passed(entry.timestamp), - # **serialize_playlist(playlist), - # }, - # } - # ) continue if entry.type == "favorite": items.append( - # { - # "type": "favorite_tracks", - # "item": { - # "help_text": "playlist", - # "count": FavoritesTable.count(), - # "time": timestamp_to_time_passed(entry.timestamp), - # }, - # } { "type": "favorite", "timestamp": entry.timestamp, @@ -221,18 +149,18 @@ def get_recently_played(limit=7, userid: int | None = None): "hash": entry.trackhash, "timestamp": entry.timestamp, } - - # track = serialize_track(t.get_best()) - # track["help_text"] = "track" - # track["time"] = timestamp_to_time_passed(entry.timestamp) - items.append(item) BATCH_SIZE = 200 current_index = 0 - entries = ScrobbleTable.get_all(0, BATCH_SIZE) - max_iterations = 20 # Safeguard against unexpected infinite loops + if len(_entries): + entries = _entries + limit = 1 + else: + entries = ScrobbleTable.get_all(0, BATCH_SIZE) + + max_iterations = 20 iterations = 0 while len(items) < limit and iterations < max_iterations: diff --git a/app/lib/index.py b/app/lib/index.py index dff48dc1..81d11fac 100644 --- a/app/lib/index.py +++ b/app/lib/index.py @@ -7,6 +7,7 @@ from app.lib.mapstuff import ( map_scrobble_data, ) from app.lib.populate import CordinateMedia +from app.lib.recipes.recents import RecentlyAdded from app.lib.tagger import IndexTracks from app.store.albums import AlbumStore from app.store.artists import ArtistStore @@ -25,6 +26,9 @@ class IndexEverything: ArtistStore.load_artists(key) FolderStore.load_filepaths() + # NOTE: Rebuild recently added items on the homepage store + RecentlyAdded() + # map colors map_album_colors() map_artist_colors() diff --git a/app/lib/recipes/recents.py b/app/lib/recipes/recents.py index 2ac314f7..12ff558d 100644 --- a/app/lib/recipes/recents.py +++ b/app/lib/recipes/recents.py @@ -1,5 +1,5 @@ import pprint -from app.db.userdata import UserTable +from app.db.userdata import ScrobbleTable, UserTable from app.lib.home.recentlyadded import get_recently_added_items from app.lib.home.recentlyplayed import get_recently_played from app.lib.recipes import HomepageRoutine @@ -7,6 +7,7 @@ from app.store.homepage import HomepageStore class RecentlyPlayed(HomepageRoutine): + ITEM_LIMIT = 15 store_key = "recently_played" def __init__(self, userid: int | None = None) -> None: @@ -15,6 +16,11 @@ class RecentlyPlayed(HomepageRoutine): outside a cron job. ie. when a user records a new scrobble. """ self.userids = [userid] if userid else [user.id for user in UserTable.get_all()] + + # NOTE: When the userid is provided + # we need to update the store for that userid only + # using the last scrobble entry. + self.update_only = userid is not None super().__init__() @property @@ -22,8 +28,47 @@ class RecentlyPlayed(HomepageRoutine): return True def run(self): + if self.update_only: + last_entry = ScrobbleTable.get_last_entry(self.userids[0]) + + if last_entry: + items = get_recently_played( + limit=self.ITEM_LIMIT, userid=self.userids[0], _entries=[last_entry] + ) + + item = items[0] + store_entry = HomepageStore.entries[self.store_key].items[ + self.userids[0] + ][0] + + if ( + item["type"] + item["hash"] + == store_entry["type"] + store_entry["hash"] + ): + # If the item is the same as the one in the store + # only update the timestamp + HomepageStore.entries[self.store_key].items[self.userids[0]][0][ + "timestamp" + ] = item["timestamp"] + else: + # Otherwise, insert the new item + # and remove the oldest item if there are more than 15 items + HomepageStore.entries[self.store_key].items[self.userids[0]].insert( + 0, item + ) + + if ( + len( + HomepageStore.entries[self.store_key].items[self.userids[0]] + ) + > self.ITEM_LIMIT + ): + HomepageStore.entries[self.store_key].items[ + self.userids[0] + ].pop() + for userid in self.userids: - items = get_recently_played(limit=15, userid=userid) + items = get_recently_played(limit=self.ITEM_LIMIT, userid=userid) HomepageStore.entries[self.store_key].items[userid] = items