modularize src

+ merge main.py and manage.py
+ move start logic to swingmusic/__main__.py
+ add a run.py on the project root
This commit is contained in:
cwilvx
2025-05-25 20:35:54 +03:00
parent 76fc97e088
commit 86fabcd5e3
171 changed files with 658 additions and 627 deletions
+29
View File
@@ -0,0 +1,29 @@
"""
Recipes are a way to create mixes.
"""
from abc import ABC, abstractmethod
from typing import Any, List
class HomepageRoutine(ABC):
"""
A routine creates a row of homepage items.
"""
@property
@abstractmethod
def is_valid(self) -> bool: ...
def __init__(self) -> None:
if not self.is_valid:
return
self.run()
@abstractmethod
def run(self) -> List[Any]:
"""
Creates the homepage items and saves them to the
homepage store if self.is_valid is true.
"""
...
+38
View File
@@ -0,0 +1,38 @@
from swingmusic.db.userdata import UserTable
from swingmusic.lib.recipes import HomepageRoutine
from swingmusic.plugins.mixes import MixesPlugin
from swingmusic.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)
custom_mixes = []
for _mix in mixes:
custom_mix = MixesPlugin.get_track_mix(_mix)
if custom_mix:
custom_mixes.append(custom_mix)
HomepageStore.set_mixes(
custom_mixes, entrykey="custom_mixes", userid=user.id
)
def __init__(self) -> None:
super().__init__()
+37
View File
@@ -0,0 +1,37 @@
from pprint import pprint
from swingmusic.db.userdata import UserTable
from swingmusic.lib.recipes import HomepageRoutine
from swingmusic.lib.recipes.artistmixes import ArtistMixes
from swingmusic.models.mix import Mix
from swingmusic.plugins.mixes import MixesPlugin
from swingmusic.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
+97
View File
@@ -0,0 +1,97 @@
import pprint
from swingmusic.db.userdata import ScrobbleTable, UserTable
from swingmusic.lib.home.recentlyadded import get_recently_added_items
from swingmusic.lib.home.get_recently_played import get_recently_played
from swingmusic.lib.recipes import HomepageRoutine
from swingmusic.store.homepage import HomepageStore
class RecentlyPlayed(HomepageRoutine):
ITEM_LIMIT = 15
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()]
# 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
def is_valid(self):
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]
)
try:
item = items[0]
store_entry = HomepageStore.entries[self.store_key].items[
self.userids[0]
][0]
except IndexError:
store_entry = None
item = None
if (
store_entry
and item
and store_entry.get("type", "") + store_entry.get("hash", "")
== item.get("type", "") + item.get("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=self.ITEM_LIMIT, 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
+83
View File
@@ -0,0 +1,83 @@
from gettext import ngettext
import pendulum
from swingmusic.crons.cron import CronJob
from swingmusic.db.userdata import UserTable
from swingmusic.lib.recipes import HomepageRoutine
from swingmusic.store.homepage import HomepageStore
from swingmusic.utils.dates import get_date_range, seconds_to_time_string
from swingmusic.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 the weekend.
"""
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 (5, 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 = {}