From 809415ddb4b7be4df9501aa901f92054c14ae2af Mon Sep 17 00:00:00 2001 From: cwilvx Date: Wed, 27 Nov 2024 10:55:11 +0300 Subject: [PATCH] add because you listened to artist artists + add artists you might like --- app/crons/__init__.py | 2 +- app/crons/mixes.py | 5 +++ app/lib/home/recentlyplayed.py | 4 +-- app/lib/recipes/because.py | 37 +++++++++++++++++++++ app/plugins/mixes.py | 61 +++++++++++++++++++++++++++++++++- app/store/homepage.py | 38 ++++++++++++++++++--- 6 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 app/lib/recipes/because.py diff --git a/app/crons/__init__.py b/app/crons/__init__.py index 4d648e18..8e5c56a0 100644 --- a/app/crons/__init__.py +++ b/app/crons/__init__.py @@ -18,9 +18,9 @@ def start_cron_jobs(): RecentlyAdded() # Initialized CRON jobs - Mixes() TopArtists() TopArtists(duration="week") + Mixes() # Trigger all CRON jobs when the app is started. schedule.run_all() diff --git a/app/crons/mixes.py b/app/crons/mixes.py index 08274896..dbb596c1 100644 --- a/app/crons/mixes.py +++ b/app/crons/mixes.py @@ -1,5 +1,6 @@ from app.crons.cron import CronJob from app.lib.recipes.artistmixes import ArtistMixes +from app.lib.recipes.because import BecauseYouListened class Mixes(CronJob): @@ -19,3 +20,7 @@ class Mixes(CronJob): """ print("⭐⭐⭐⭐ Mixes cron job running") ArtistMixes() + + # INFO: Because you listened to artist items are generated using + # the artist mixes, so run them after the artist mixes are created. + BecauseYouListened() diff --git a/app/lib/home/recentlyplayed.py b/app/lib/home/recentlyplayed.py index 6aee67d1..a565ab80 100644 --- a/app/lib/home/recentlyplayed.py +++ b/app/lib/home/recentlyplayed.py @@ -184,7 +184,7 @@ def get_recently_played( return items -def recover_recently_played_items(items: list[dict]): +def recover_items(items: list[dict]): custom_playlists = [ {"name": "recentlyadded", "handler": get_recently_added_playlist}, {"name": "recentlyplayed", "handler": get_recently_played_playlist}, @@ -235,7 +235,7 @@ def recover_recently_played_items(items: list[dict]): }, } elif item["type"] == "playlist": - if item["is_custom"]: + if item.get("is_custom"): playlist, _ = next( i["handler"]() for i in custom_playlists diff --git a/app/lib/recipes/because.py b/app/lib/recipes/because.py new file mode 100644 index 00000000..2454e5b0 --- /dev/null +++ b/app/lib/recipes/because.py @@ -0,0 +1,37 @@ +from pprint import pprint +from app.db.userdata import UserTable +from app.lib.recipes import HomepageRoutine +from app.lib.recipes.artistmixes import ArtistMixes +from app.models.mix import Mix +from app.plugins.mixes import MixesPlugin +from app.store.homepage import HomepageStore + + +class BecauseYouListened(HomepageRoutine): + store_keys = ["because_you_listened_to_artist", "artists_you_might_like"] + + @property + def is_valid(self): + return MixesPlugin().enabled + + def run(self): + users = UserTable.get_all() + + for user in users: + entry: dict[str, Mix] = HomepageStore.entries.get( + ArtistMixes.store_key + ).items.get(user.id) # type: ignore + + if not entry: + continue + + because_you_listened_to_artist, artists_you_might_like = ( + MixesPlugin().get_because_items(list(entry.values())) + ) + + HomepageStore.entries[self.store_keys[0]].items[ + user.id + ] = because_you_listened_to_artist + HomepageStore.entries[self.store_keys[1]].items[ + user.id + ] = artists_you_might_like diff --git a/app/plugins/mixes.py b/app/plugins/mixes.py index d4171b17..cecbab6b 100644 --- a/app/plugins/mixes.py +++ b/app/plugins/mixes.py @@ -1,4 +1,5 @@ import datetime +from gettext import ngettext import json from pprint import pprint import random @@ -136,7 +137,7 @@ class MixesPlugin(Plugin): # try to balance the mix trackmatches = balance_mix(trackmatches) - return trackmatches, results["albums"], results["albums"] + return trackmatches, results["albums"], results["artists"] @plugin_method def get_artist_mix(self, artisthash: str): @@ -419,3 +420,61 @@ class MixesPlugin(Plugin): TODO: Implement this! """ pass + + def get_because_items(self, mixes: list[Mix]): + """ + Given a list of mixes, returns a list of artists that are similar to the + artists in the mixes. + """ + artists: dict[str, list[dict[str, str | int]]] = {} + + for mix in mixes: + mix_artisthash = mix.extra["artisthash"] + artists.setdefault(mix_artisthash, []) + + for artisthash in mix.extra["artists"]: + artist = ArtistStore.artistmap.get(artisthash) + + if not artist: + continue + + artists[mix_artisthash].append( + { + "type": "artist", + "trackcount": artist.artist.trackcount, + "hash": artisthash, + "help_text": str(artist.artist.trackcount) + + ngettext(" track", " tracks", artist.artist.trackcount), + } + ) + + # INFO: Sort artists by trackcount + artists[mix_artisthash] = sorted( + artists[mix_artisthash], + key=lambda x: x["trackcount"], + reverse=True, + ) + + artisthash = mixes[0].extra["artisthash"] + because_you_listened_to_artist = { + "title": "Because you listened to " + + ArtistStore.artistmap[artisthash].artist.name, + "items": artists[artisthash][:15], + } + + # Flatten list of artists and remove duplicates by artisthash + all_artists = [] + seen = set() + + for artist_list in artists.values(): + for artist in artist_list: + if artist["hash"] not in seen: + all_artists.append(artist) + seen.add(artist["hash"]) + + artists_you_might_like = { + "title": "Artists you might like", + "items": random.sample(all_artists, k=min(15, len(all_artists))), + } + + return because_you_listened_to_artist, artists_you_might_like diff --git a/app/store/homepage.py b/app/store/homepage.py index 12fb8808..28bfab69 100644 --- a/app/store/homepage.py +++ b/app/store/homepage.py @@ -1,7 +1,8 @@ from abc import ABC from dataclasses import dataclass +import pprint from typing import Any -from app.lib.home.recentlyplayed import recover_recently_played_items +from app.lib.home.recentlyplayed import recover_items from app.models.mix import Mix from app.utils.auth import get_current_userid @@ -78,7 +79,7 @@ class RecentlyPlayedHomepageEntry(HomepageEntry): return { "title": self.title, "description": self.description, - "items": recover_recently_played_items(items), + "items": recover_items(items), } @@ -91,7 +92,7 @@ class RecentlyAddedHomepageEntry(RecentlyPlayedHomepageEntry): return super().get_items(0, limit) -class TopStreamedHomepageEntry(RecentlyPlayedHomepageEntry): +class GenericRecoverableEntry(RecentlyPlayedHomepageEntry): """ A homepage entry for top streamed. """ @@ -101,6 +102,25 @@ class TopStreamedHomepageEntry(RecentlyPlayedHomepageEntry): pass +class BecauseYouListenedToArtistHomepageEntry(RecentlyPlayedHomepageEntry): + """ + A homepage entry for because you listened to artist. + """ + + # SHAPE: {userid: {title: str, items: list[RecoverableItem]}} + items: dict[int, dict[str, Any]] + + def get_items(self, userid: int, limit: int | None = None): + pprint.pprint(self.items) + title = self.items.get(userid, {}).get("title") + items = self.items.get(userid, {}).get("items", [])[:limit] + + return { + "title": title, + "items": recover_items(items), + } + + class HomepageStore: """ Stores the homepage items. @@ -114,14 +134,22 @@ class HomepageStore: title="Artist mixes for you", description="Based on artists you have been listening to", ), - "top_streamed_weekly_artists": TopStreamedHomepageEntry( + "top_streamed_weekly_artists": GenericRecoverableEntry( title="Top artists this week", description="Your most played artists since Monday", ), - "top_streamed_monthly_artists": TopStreamedHomepageEntry( + "top_streamed_monthly_artists": GenericRecoverableEntry( title="Top artists this month", description="Your most played artists since the start of the month", ), + "because_you_listened_to_artist": BecauseYouListenedToArtistHomepageEntry( + title="", + description="Artists similar to the artist you listened to", + ), + "artists_you_might_like": BecauseYouListenedToArtistHomepageEntry( + title="Artists you might like", + description="Artists similar to the artists you have listened to", + ), "recently_added": RecentlyAddedHomepageEntry( title="Recently added", description="New music added to your library",