diff --git a/app/api/home/__init__.py b/app/api/home/__init__.py index 1179c555..e7fa192a 100644 --- a/app/api/home/__init__.py +++ b/app/api/home/__init__.py @@ -35,6 +35,4 @@ class HomepageItem(BaseModel): @api.get("/") def homepage_items(query: HomepageItem): - return { - "artist_mixes": HomepageStore.get_mixes("artist_mixes", limit=query.limit), - } + return HomepageStore.get_homepage_items(limit=query.limit) \ No newline at end of file diff --git a/app/crons/__init__.py b/app/crons/__init__.py index 1c72e96a..41e09ed3 100644 --- a/app/crons/__init__.py +++ b/app/crons/__init__.py @@ -2,6 +2,8 @@ import time import schedule from app.crons.mixes import Mixes +from app.lib.recipes.recents import RecentlyAdded, RecentlyPlayed +from app.lib.recipes.topstreamed import TopArtists from app.utils.threading import background @@ -10,9 +12,20 @@ def start_cron_jobs(): """ This is the function that triggers the cron jobs. """ - Mixes() + # NOTE: RecentlyPlayed is not a CRON job, it's triggered here to + # populate the values for the very first time. + RecentlyPlayed() + RecentlyAdded() + + # Initialized CRON jobs + # Mixes() + TopArtists() + TopArtists(duration="week") + + # Trigger all CRON jobs when the app is started. schedule.run_all() + # Run all CRON jobs on a loop. while True: schedule.run_pending() time.sleep(1) diff --git a/app/crons/cron.py b/app/crons/cron.py index 35954768..4f9e02ad 100644 --- a/app/crons/cron.py +++ b/app/crons/cron.py @@ -9,10 +9,10 @@ class CronJob(ABC): A cron job that will be run on a regular interval. """ - def __init__(self, name: str, hours: int): - self.name = name - self.hours = hours + name: str + hours: int = 1 + def __init__(self): schedule.every(self.hours).hours.do(self.run) @abstractmethod diff --git a/app/crons/mixes.py b/app/crons/mixes.py index 884cfa87..db8c2773 100644 --- a/app/crons/mixes.py +++ b/app/crons/mixes.py @@ -1,7 +1,5 @@ from app.crons.cron import CronJob -from app.lib.recipes import ArtistMixes -from app.plugins.mixes import MixesPlugin -from app.store.homepage import HomepageStore +from app.lib.recipes.artistmixes import ArtistMixes class Mixes(CronJob): @@ -9,22 +7,15 @@ class Mixes(CronJob): This cron job creates mixes displayed on the homepage. """ + name: str = "mixes" + hours: int = 1 + def __init__(self): - super().__init__("mixes", 1) + super().__init__() def run(self): """ Creates the artist mixes """ print("⭐⭐⭐⭐ Mixes cron job running") - ArtistMixes().run() - # mixes = MixesPlugin() - - # if not mixes.enabled: - # return - - - # artist_mixes = mixes.create_artist_mixes() - - # if artist_mixes: - # HomepageStore.set_artist_mixes(artist_mixes) + ArtistMixes() diff --git a/app/db/userdata.py b/app/db/userdata.py index 2ec438d8..8cc0fd54 100644 --- a/app/db/userdata.py +++ b/app/db/userdata.py @@ -293,10 +293,10 @@ class ScrobbleTable(Base): return cls.insert_one(item) @classmethod - def get_all(cls, start: int, limit: int | None = None): + def get_all(cls, start: int, limit: int | None = None, userid: int | None = None): result = cls.execute( select(cls) - .where(cls.userid == get_current_userid()) + .where(cls.userid == (userid if userid else get_current_userid())) .order_by(cls.timestamp.desc()) .offset(start) .limit(limit) diff --git a/app/lib/home/recentlyadded.py b/app/lib/home/recentlyadded.py index abb93ae6..58ef1184 100644 --- a/app/lib/home/recentlyadded.py +++ b/app/lib/home/recentlyadded.py @@ -59,13 +59,19 @@ def create_track(t: Track): """ Creates a recently added track entry. """ - track = serialize_track(t, to_remove={"created_date"}) - track["help_text"] = "NEW TRACK" - return { "type": "track", - "item": track, + "hash": t.trackhash, + "timestamp": t.last_mod, + "help_text": "NEW TRACK", } + # track = serialize_track(t, to_remove={"created_date"}) + # track["help_text"] = "NEW TRACK" + + # return { + # "type": "track", + # "item": track, + # } # INFO: Keys: folder, tracks, time (timestamp) @@ -94,26 +100,25 @@ def check_folder_type(group_: dict): if entry is None: return None - album = album_serializer( - entry.album, - to_remove={ - "genres", - "og_title", - "date", - "duration", - "count", - "albumartists_hashes", - "base_title", - }, - ) - album["help_text"] = ( - "NEW ALBUM" if albumhash in existing_album_hashes else "NEW TRACKS" - ) - album["time"] = timestamp_to_time_passed(time) - + # album = album_serializer( + # entry.album, + # to_remove={ + # "genres", + # "og_title", + # "date", + # "duration", + # "count", + # "albumartists_hashes", + # "base_title", + # }, + # ) return { "type": "album", - "item": album, + "hash": albumhash, + "timestamp": time, + "help_text": ( + "NEW ALBUM" if albumhash in existing_album_hashes else "NEW TRACKS" + ), } is_artist, artisthash, trackcount = check_is_artist_folder(tracks) @@ -122,19 +127,28 @@ def check_folder_type(group_: dict): if entry is None: return None - - artist = serialize_for_card(entry.artist) - artist["trackcount"] = trackcount - artist["help_text"] = ( - "NEW ARTIST" if artisthash not in existing_artist_hashes else "NEW MUSIC" - ) - artist["time"] = timestamp_to_time_passed(time) - + return { "type": "artist", - "item": artist, + "hash": artisthash, + "timestamp": time, + "help_text": ( + "NEW ARTIST" if artisthash not in existing_artist_hashes else "NEW MUSIC" + ), } + # artist = serialize_for_card(entry.artist) + # artist["trackcount"] = trackcount + # artist["help_text"] = ( + # "NEW ARTIST" if artisthash not in existing_artist_hashes else "NEW MUSIC" + # ) + # artist["time"] = timestamp_to_time_passed(time) + + # return { + # "type": "artist", + # "item": artist, + # } + is_track_folder = check_is_track_folder(tracks) return ( @@ -142,12 +156,15 @@ def check_folder_type(group_: dict): if is_track_folder else { "type": "folder", - "item": { - "path": key, - "count": len(tracks), - "help_text": "NEW MUSIC", - "time": timestamp_to_time_passed(time), - }, + "hash": key, + "timestamp": time, + "help_text": "NEW MUSIC", + # "item": { + # "path": key, + # "count": len(tracks), + # "help_text": "NEW MUSIC", + # "time": timestamp_to_time_passed(time), + # }, } ) diff --git a/app/lib/home/recentlyplayed.py b/app/lib/home/recentlyplayed.py index dcf00459..0476d26b 100644 --- a/app/lib/home/recentlyplayed.py +++ b/app/lib/home/recentlyplayed.py @@ -22,7 +22,7 @@ from app.store.tracks import TrackStore from app.store.artists import ArtistStore -def get_recently_played(limit=7): +def get_recently_played(limit=7, userid: int | None = None): # TODO: Paginate this items = [] added = set() @@ -43,47 +43,52 @@ def get_recently_played(limit=7): added.add(entry.source) if entry.type == "album": - album = AlbumStore.get_album_by_hash(entry.type_src) + album = AlbumStore.albummap.get(entry.type_src) if album is None: continue - album = album_serializer( - album, - to_remove={ - "genres", - "date", - "count", - "duration", - "albumartists_hashes", - "og_title", - }, - ) - album["help_text"] = "album" - album["time"] = timestamp_to_time_passed(entry.timestamp) + # 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) - items.append( - { - "type": "album", - "item": album, - } - ) + # { + # "type": "album", + # "item": album, + # } + items.append(item) continue if entry.type == "artist": - artist = ArtistStore.get_artist_by_hash(entry.type_src) + artist = ArtistStore.artistmap.get(entry.type_src) if artist is None: continue - artist = serialize_for_card(artist) - artist["help_text"] = "artist" - artist["time"] = timestamp_to_time_passed(entry.timestamp) + # artist = serialize_for_card(artist) + # artist["help_text"] = "artist" + # artist["time"] = timestamp_to_time_passed(entry.timestamp) items.append( { "type": "artist", - "item": artist, + "hash": entry.type_src, + "timestamp": entry.timestamp, } ) @@ -103,47 +108,56 @@ def get_recently_played(limit=7): if is_home_dir: folder = os.path.expanduser("~") - # print(folder) - # folder = os.path.join("/", folder, "") - # print(folder) - # count = len([t for t in TrackStore.tracks if t.folder == folder]) - count = FolderStore.count_tracks_containing_paths([folder]) - items.append( - { - "type": "folder", - "item": { - "path": folder, - "count": count[0]["trackcount"], - "help_text": "folder", - "time": timestamp_to_time_passed(entry.timestamp), - }, - } - ) + # count = FolderStore.count_tracks_containing_paths([folder]) + item = { + "type": "folder", + "hash": entry.type_src, + "timestamp": entry.timestamp, + } + + 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] - # is_recently_added = entry.type_src == "recentlyadded" 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, _ = 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 = serialize_playlist( + # playlist, to_remove={"settings", "duration"} + # ) - playlist["help_text"] = "playlist" - playlist["time"] = timestamp_to_time_passed(entry.timestamp) + # playlist["help_text"] = "playlist" + # playlist["time"] = timestamp_to_time_passed(entry.timestamp) + # items.append( + # { + # "type": "playlist", + # "item": playlist, + # } + # ) items.append( { "type": "playlist", - "item": playlist, + "hash": entry.type_src, + "timestamp": entry.timestamp, + "is_custom": True, } ) continue @@ -152,34 +166,47 @@ def get_recently_played(limit=7): if playlist is None: continue - tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes) - playlist.clear_lists() + item = { + "type": "playlist", + "hash": entry.type_src, + "timestamp": entry.timestamp, + } - if not playlist.has_image: - images = get_first_4_images(tracks) - images = [i["image"] for i in images] - playlist.images = images + items.append(item) - items.append( - { - "type": "playlist", - "item": { - "help_text": "playlist", - "time": timestamp_to_time_passed(entry.timestamp), - **serialize_playlist(playlist), - }, - } - ) + # 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_tracks", - "item": { - "help_text": "playlist", - "count": FavoritesTable.count(), - "time": timestamp_to_time_passed(entry.timestamp), - }, + "type": "favorite", + "timestamp": entry.timestamp, } ) continue @@ -189,22 +216,23 @@ def get_recently_played(limit=7): if t is None: continue - track = serialize_track(t.get_best()) - track["help_text"] = "track" - track["time"] = timestamp_to_time_passed(entry.timestamp) + item = { + "type": "track", + "hash": entry.trackhash, + "timestamp": entry.timestamp, + } - items.append( - { - "type": "track", - "item": track, - } - ) + # 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 + max_iterations = 20 # Safeguard against unexpected infinite loops iterations = 0 while len(items) < limit and iterations < max_iterations: @@ -212,7 +240,9 @@ def get_recently_played(limit=7): current_index += BATCH_SIZE if len(items) < limit: - entries = ScrobbleTable.get_all(current_index + 1, BATCH_SIZE) + entries = ScrobbleTable.get_all( + start=current_index + 1, limit=BATCH_SIZE, userid=userid + ) if not entries: break @@ -226,6 +256,126 @@ def get_recently_played(limit=7): return items +def recover_recently_played_items(items: list[dict]): + custom_playlists = [ + {"name": "recentlyadded", "handler": get_recently_added_playlist}, + {"name": "recentlyplayed", "handler": get_recently_played_playlist}, + ] + recovered = [] + + for item in items: + recovered_item = None + + if item["type"] == "album": + album = AlbumStore.get_album_by_hash(item["hash"]) + if album is None: + continue + + album = album_serializer( + album, + to_remove={ + "genres", + "date", + "count", + "duration", + "albumartists_hashes", + "og_title", + }, + ) + + recovered_item = { + "type": "album", + "item": album, + } + elif item["type"] == "artist": + artist = ArtistStore.get_artist_by_hash(item["hash"]) + if artist is None: + continue + + recovered_item = { + "type": "artist", + "item": serialize_for_card(artist), + } + elif item["type"] == "folder": + count = FolderStore.count_tracks_containing_paths([item["hash"]]) + + recovered_item = { + "type": "folder", + "item": { + "path": item["hash"], + "count": count[0]["trackcount"], + }, + } + elif item["type"] == "playlist": + if item["is_custom"]: + playlist, _ = next( + i["handler"]() + for i in custom_playlists + if i["name"] == item["hash"] + ) + playlist.images = [i["image"] for i in playlist.images] + + playlist = serialize_playlist( + playlist, to_remove={"settings", "duration"} + ) + recovered_item = { + "type": "playlist", + "item": playlist, + } + else: + playlist = PlaylistTable.get_by_id(item["hash"]) + if playlist is None: + continue + + 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 + + recovered_item = { + "type": "playlist", + "item": serialize_playlist(playlist), + } + elif item["type"] == "favorite": + recovered_item = { + "type": "favorite", + "item": { + "count": FavoritesTable.count(), + }, + } + elif item["type"] == "track": + track = TrackStore.trackhashmap.get(item["hash"]) + if track is None: + continue + + recovered_item = { + "type": "track", + "item": serialize_track(track.get_best()), + } + + if recovered_item is not None: + helptext = item.get("help_text") or item.get("type") + secondary_text = item.get("secondary_text") + + if "secondary_text" in item: + secondary_text = item["secondary_text"] + elif "timestamp" in item: + secondary_text = timestamp_to_time_passed(item["timestamp"]) + + if helptext: + recovered_item["item"]["help_text"] = helptext + + if secondary_text: + recovered_item["item"]["time"] = secondary_text + + recovered.append(recovered_item) + + return recovered + + def get_recently_played_playlist(limit: int = 100): playlist = Playlist( id="recentlyplayed", diff --git a/app/lib/recipes/__init__.py b/app/lib/recipes/__init__.py index 9065eb2e..b096c680 100644 --- a/app/lib/recipes/__init__.py +++ b/app/lib/recipes/__init__.py @@ -3,25 +3,13 @@ Recipes are a way to create mixes. """ from abc import ABC, abstractmethod -from typing import Any, Dict, List - -from app.db.userdata import UserTable -from app.models.mix import Mix -from app.plugins.mixes import MixesPlugin -from app.store.homepage import HomepageStore - +from typing import Any, List class HomepageRoutine(ABC): """ A routine creates a row of homepage items. """ - title: str - description: str - - items: List[Mix] - extra: Dict[str, Any] - @property @abstractmethod def is_valid(self) -> bool: ... @@ -30,37 +18,12 @@ class HomepageRoutine(ABC): if not self.is_valid: return - self.items = self.run() + self.run() @abstractmethod - def run(self) -> List[Mix]: + def run(self) -> List[Any]: """ Creates the homepage items and saves them to the homepage store if self.is_valid is true. """ ... - - -class ArtistMixes(HomepageRoutine): - items: List[Mix] = [] - extra: Dict[str, Any] = {} - store_key = "artist_mixes" - - @property - def is_valid(self): - return MixesPlugin().enabled - - def run(self): - users = UserTable.get_all() - - for user in users: - mix = MixesPlugin() - mixes = mix.create_artist_mixes(user.id) - - if not mixes: - continue - - HomepageStore.set_mixes(mixes, mixkey=self.store_key, userid=user.id) - - def __init__(self) -> None: - super().__init__() diff --git a/app/lib/recipes/artistmixes.py b/app/lib/recipes/artistmixes.py new file mode 100644 index 00000000..ac0d2199 --- /dev/null +++ b/app/lib/recipes/artistmixes.py @@ -0,0 +1,27 @@ +from app.db.userdata import UserTable +from app.lib.recipes import HomepageRoutine +from app.plugins.mixes import MixesPlugin +from app.store.homepage import HomepageStore + + +class ArtistMixes(HomepageRoutine): + store_key = "artist_mixes" + + @property + def is_valid(self): + return MixesPlugin().enabled + + def run(self): + users = UserTable.get_all() + + for user in users: + mix = MixesPlugin() + mixes = mix.create_artist_mixes(user.id) + + if not mixes: + continue + + HomepageStore.set_mixes(mixes, entrykey=self.store_key, userid=user.id) + + def __init__(self) -> None: + super().__init__() \ No newline at end of file diff --git a/app/lib/recipes/recents.py b/app/lib/recipes/recents.py new file mode 100644 index 00000000..2ac314f7 --- /dev/null +++ b/app/lib/recipes/recents.py @@ -0,0 +1,46 @@ +import pprint +from app.db.userdata import 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 +from app.store.homepage import HomepageStore + + +class RecentlyPlayed(HomepageRoutine): + store_key = "recently_played" + + def __init__(self, userid: int | None = None) -> None: + """ + The userid is provided when we are running this routine + 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()] + super().__init__() + + @property + def is_valid(self): + return True + + def run(self): + for userid in self.userids: + items = get_recently_played(limit=15, userid=userid) + HomepageStore.entries[self.store_key].items[userid] = items + + +class RecentlyAdded(HomepageRoutine): + ITEM_LIMIT = 15 + store_key = "recently_added" + + @property + def is_valid(self): + return True + + def __init__(self): + super().__init__() + + def run(self): + items = get_recently_added_items(limit=self.ITEM_LIMIT) + + # NOTE: Recently added is a global entry + # So we don't need a userid + HomepageStore.entries[self.store_key].items[0] = items diff --git a/app/lib/recipes/topstreamed.py b/app/lib/recipes/topstreamed.py new file mode 100644 index 00000000..9907d541 --- /dev/null +++ b/app/lib/recipes/topstreamed.py @@ -0,0 +1,84 @@ +from gettext import ngettext +from os import name +import pendulum + +from app.crons.cron import CronJob +from app.db.userdata import UserTable +from app.lib.recipes import HomepageRoutine +from app.store.homepage import HomepageStore +from app.utils.dates import get_date_range, seconds_to_time_string +from app.utils.stats import get_artists_in_period + + +class TopArtists(CronJob, HomepageRoutine): + """ + A routine to populate the top streamed artists/albums in the last week or month + """ + + hours = 1 + ITEM_LIMIT = 15 + + @property + def is_valid(self): + """ + Only valid if it's the middle or last 2 days of this month. + + When the duration is "week", it's valid on Saturday and Sunday. + """ + if self.duration == "month": + now = pendulum.now() + middle_day = now.days_in_month // 2 + + return ( + now.day in range(middle_day, middle_day + 2) + or now.day > now.days_in_month - 2 + ) + if self.duration == "week": + return pendulum.now().isoweekday() in (6, 7) + + return False + + def __init__(self, duration: str = "month") -> None: + super().__init__() + self.duration = duration + + if not self.is_valid: + return + + def run(self): + if not self.is_valid: + self.destroy() + return + + self.userids = [user.id for user in UserTable.get_all()] + + for userid in self.userids: + date_range = get_date_range(self.duration) + artists = get_artists_in_period(date_range[0], date_range[1], userid)[ + : self.ITEM_LIMIT + ] + + artists = [ + { + "type": "artist", + "hash": artist["artisthash"], + "help_text": seconds_to_time_string(artist["playduration"]), + "secondary_text": str(artist["playcount"]) + + " " + + ngettext("play", "plays", artist["playcount"]), + } + for artist in artists + ] + + HomepageStore.entries[f"top_streamed_{self.duration}ly_artists"].items[ + userid + ] = artists + + def destroy(self): + """ + Clear the top streamed entry from the homepage store. + """ + keys = [f"top_streamed_{self.duration}ly_artists"] + + for key in keys: + HomepageStore.entries[key].items = {} diff --git a/app/models/logger.py b/app/models/logger.py index e4b6e368..e3668ea9 100644 --- a/app/models/logger.py +++ b/app/models/logger.py @@ -13,11 +13,17 @@ class TrackLog: duration: int timestamp: int source: str + """ + The full source string, eg. "al:123456" + """ userid: int extra: dict[str, Any] type = "track" type_src = None + """ + The source identifier string, eg. albumhash, artisthash, etc. + """ def __post_init__(self): prefix_map = { diff --git a/app/store/homepage.py b/app/store/homepage.py index 02aa9ed3..12fb8808 100644 --- a/app/store/homepage.py +++ b/app/store/homepage.py @@ -1,11 +1,11 @@ from abc import ABC from dataclasses import dataclass from typing import Any +from app.lib.home.recentlyplayed import recover_recently_played_items from app.models.mix import Mix from app.utils.auth import get_current_userid -@dataclass class HomepageEntry(ABC): """ Base class for all homepage entries. @@ -15,20 +15,19 @@ class HomepageEntry(ABC): title: str description: str - items: dict[int, dict[str, Any]] + items: dict[int, Any] def __init__(self, title: str, description: str): self.title = title self.description = description - def get_items(self, userid: int): + def get_items(self, userid: int, limit: int | None = None): """ Return usable items for the homepage. """ ... -@dataclass class MixHomepageEntry(HomepageEntry): """ A homepage entry for mixes. @@ -62,26 +61,77 @@ class MixHomepageEntry(HomepageEntry): } +class RecentlyPlayedHomepageEntry(HomepageEntry): + """ + A homepage entry for recently played. + """ + + items: dict[int, list[dict[str, Any]]] + + def __init__(self, title: str, description: str = ""): + super().__init__(title, description) + self.items = {} + + def get_items(self, userid: int, limit: int | None = None): + items = self.items.get(userid, [])[:limit] + + return { + "title": self.title, + "description": self.description, + "items": recover_recently_played_items(items), + } + + +class RecentlyAddedHomepageEntry(RecentlyPlayedHomepageEntry): + """ + A homepage entry for recently added. + """ + + def get_items(self, userid: int, limit: int | None = None): + return super().get_items(0, limit) + + +class TopStreamedHomepageEntry(RecentlyPlayedHomepageEntry): + """ + A homepage entry for top streamed. + """ + + # NOTE: This extends RecentlyPlayedHomepageEntry because + # the shape of the data is the same. + pass + + class HomepageStore: """ Stores the homepage items. """ - entries = { + entries: dict[str, HomepageEntry] = { + "recently_played": RecentlyPlayedHomepageEntry( + title="Recently played", + ), "artist_mixes": MixHomepageEntry( title="Artist mixes for you", description="Based on artists you have been listening to", ), + "top_streamed_weekly_artists": TopStreamedHomepageEntry( + title="Top artists this week", + description="Your most played artists since Monday", + ), + "top_streamed_monthly_artists": TopStreamedHomepageEntry( + title="Top artists this month", + description="Your most played artists since the start of the month", + ), + "recently_added": RecentlyAddedHomepageEntry( + title="Recently added", + description="New music added to your library", + ), } @classmethod - def set_mixes(cls, mixes: list[Mix], mixkey: str, userid: int | None = None): - idmap = {mix.id[1:]: mix for mix in mixes} - cls.entries[mixkey].items[userid or get_current_userid()] = idmap - - @classmethod - def get_mixes(cls, mixkey: str, limit: int | None = 9): - return cls.entries[mixkey].get_items(get_current_userid(), limit) + def set_mixes(cls, items: list[Any], entrykey: str, userid: int | None = None): + idmap = {item.id[1:]: item for item in items} + cls.entries[entrykey].items[userid or get_current_userid()] = idmap @classmethod def get_mix(cls, mixkey: str, mixid: str): @@ -89,14 +139,10 @@ class HomepageStore: return mix.to_full_dict() if mix else None @classmethod - def get_mix_by_sourcehash(cls, sourcehash: str): - return next( - ( - mix - for mix in cls.entries["artist_mixes"] - .items.get(get_current_userid(), {}) - .values() - if mix.sourcehash == sourcehash - ), - None, - ) + def get_homepage_items(cls, limit: int): + # return a dict of entry name to entry items + return [ + {entry: cls.entries[entry].get_items(get_current_userid(), limit)} + for entry in cls.entries.keys() + if len(cls.entries[entry].items) + ] diff --git a/app/utils/dates.py b/app/utils/dates.py index c7b5306f..67e77ef7 100644 --- a/app/utils/dates.py +++ b/app/utils/dates.py @@ -46,7 +46,7 @@ def date_string_to_time_passed(prev_date: str) -> str: return timestamp_to_time_passed(then) -def seconds_to_time_string(seconds): +def seconds_to_time_string(seconds: int): """ Converts seconds to a time string. e.g. 1 hour 2 minutes, 1 hour 2 seconds, 1 hour, 1 minute 2 seconds, etc. """