mirror of
https://github.com/Dvorinka/swingmusic-extended.git
synced 2026-06-05 04:53:01 +00:00
move recently added to routines
This commit is contained in:
@@ -35,6 +35,4 @@ class HomepageItem(BaseModel):
|
|||||||
|
|
||||||
@api.get("/")
|
@api.get("/")
|
||||||
def homepage_items(query: HomepageItem):
|
def homepage_items(query: HomepageItem):
|
||||||
return {
|
return HomepageStore.get_homepage_items(limit=query.limit)
|
||||||
"artist_mixes": HomepageStore.get_mixes("artist_mixes", limit=query.limit),
|
|
||||||
}
|
|
||||||
+14
-1
@@ -2,6 +2,8 @@ import time
|
|||||||
import schedule
|
import schedule
|
||||||
|
|
||||||
from app.crons.mixes import Mixes
|
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
|
from app.utils.threading import background
|
||||||
|
|
||||||
|
|
||||||
@@ -10,9 +12,20 @@ def start_cron_jobs():
|
|||||||
"""
|
"""
|
||||||
This is the function that triggers the 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()
|
schedule.run_all()
|
||||||
|
|
||||||
|
# Run all CRON jobs on a loop.
|
||||||
while True:
|
while True:
|
||||||
schedule.run_pending()
|
schedule.run_pending()
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|||||||
+3
-3
@@ -9,10 +9,10 @@ class CronJob(ABC):
|
|||||||
A cron job that will be run on a regular interval.
|
A cron job that will be run on a regular interval.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, hours: int):
|
name: str
|
||||||
self.name = name
|
hours: int = 1
|
||||||
self.hours = hours
|
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
schedule.every(self.hours).hours.do(self.run)
|
schedule.every(self.hours).hours.do(self.run)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|||||||
+6
-15
@@ -1,7 +1,5 @@
|
|||||||
from app.crons.cron import CronJob
|
from app.crons.cron import CronJob
|
||||||
from app.lib.recipes import ArtistMixes
|
from app.lib.recipes.artistmixes import ArtistMixes
|
||||||
from app.plugins.mixes import MixesPlugin
|
|
||||||
from app.store.homepage import HomepageStore
|
|
||||||
|
|
||||||
|
|
||||||
class Mixes(CronJob):
|
class Mixes(CronJob):
|
||||||
@@ -9,22 +7,15 @@ class Mixes(CronJob):
|
|||||||
This cron job creates mixes displayed on the homepage.
|
This cron job creates mixes displayed on the homepage.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
name: str = "mixes"
|
||||||
|
hours: int = 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__("mixes", 1)
|
super().__init__()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
Creates the artist mixes
|
Creates the artist mixes
|
||||||
"""
|
"""
|
||||||
print("⭐⭐⭐⭐ Mixes cron job running")
|
print("⭐⭐⭐⭐ Mixes cron job running")
|
||||||
ArtistMixes().run()
|
ArtistMixes()
|
||||||
# mixes = MixesPlugin()
|
|
||||||
|
|
||||||
# if not mixes.enabled:
|
|
||||||
# return
|
|
||||||
|
|
||||||
|
|
||||||
# artist_mixes = mixes.create_artist_mixes()
|
|
||||||
|
|
||||||
# if artist_mixes:
|
|
||||||
# HomepageStore.set_artist_mixes(artist_mixes)
|
|
||||||
|
|||||||
+2
-2
@@ -293,10 +293,10 @@ class ScrobbleTable(Base):
|
|||||||
return cls.insert_one(item)
|
return cls.insert_one(item)
|
||||||
|
|
||||||
@classmethod
|
@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(
|
result = cls.execute(
|
||||||
select(cls)
|
select(cls)
|
||||||
.where(cls.userid == get_current_userid())
|
.where(cls.userid == (userid if userid else get_current_userid()))
|
||||||
.order_by(cls.timestamp.desc())
|
.order_by(cls.timestamp.desc())
|
||||||
.offset(start)
|
.offset(start)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
|
|||||||
@@ -59,13 +59,19 @@ def create_track(t: Track):
|
|||||||
"""
|
"""
|
||||||
Creates a recently added track entry.
|
Creates a recently added track entry.
|
||||||
"""
|
"""
|
||||||
track = serialize_track(t, to_remove={"created_date"})
|
|
||||||
track["help_text"] = "NEW TRACK"
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "track",
|
"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)
|
# INFO: Keys: folder, tracks, time (timestamp)
|
||||||
@@ -94,26 +100,25 @@ def check_folder_type(group_: dict):
|
|||||||
if entry is None:
|
if entry is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
album = album_serializer(
|
# album = album_serializer(
|
||||||
entry.album,
|
# entry.album,
|
||||||
to_remove={
|
# to_remove={
|
||||||
"genres",
|
# "genres",
|
||||||
"og_title",
|
# "og_title",
|
||||||
"date",
|
# "date",
|
||||||
"duration",
|
# "duration",
|
||||||
"count",
|
# "count",
|
||||||
"albumartists_hashes",
|
# "albumartists_hashes",
|
||||||
"base_title",
|
# "base_title",
|
||||||
},
|
# },
|
||||||
)
|
# )
|
||||||
album["help_text"] = (
|
|
||||||
"NEW ALBUM" if albumhash in existing_album_hashes else "NEW TRACKS"
|
|
||||||
)
|
|
||||||
album["time"] = timestamp_to_time_passed(time)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"type": "album",
|
"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)
|
is_artist, artisthash, trackcount = check_is_artist_folder(tracks)
|
||||||
@@ -123,18 +128,27 @@ def check_folder_type(group_: dict):
|
|||||||
if entry is None:
|
if entry is None:
|
||||||
return 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 {
|
return {
|
||||||
"type": "artist",
|
"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)
|
is_track_folder = check_is_track_folder(tracks)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -142,12 +156,15 @@ def check_folder_type(group_: dict):
|
|||||||
if is_track_folder
|
if is_track_folder
|
||||||
else {
|
else {
|
||||||
"type": "folder",
|
"type": "folder",
|
||||||
"item": {
|
"hash": key,
|
||||||
"path": key,
|
"timestamp": time,
|
||||||
"count": len(tracks),
|
"help_text": "NEW MUSIC",
|
||||||
"help_text": "NEW MUSIC",
|
# "item": {
|
||||||
"time": timestamp_to_time_passed(time),
|
# "path": key,
|
||||||
},
|
# "count": len(tracks),
|
||||||
|
# "help_text": "NEW MUSIC",
|
||||||
|
# "time": timestamp_to_time_passed(time),
|
||||||
|
# },
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+238
-88
@@ -22,7 +22,7 @@ from app.store.tracks import TrackStore
|
|||||||
from app.store.artists import ArtistStore
|
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
|
# TODO: Paginate this
|
||||||
items = []
|
items = []
|
||||||
added = set()
|
added = set()
|
||||||
@@ -43,47 +43,52 @@ def get_recently_played(limit=7):
|
|||||||
added.add(entry.source)
|
added.add(entry.source)
|
||||||
|
|
||||||
if entry.type == "album":
|
if entry.type == "album":
|
||||||
album = AlbumStore.get_album_by_hash(entry.type_src)
|
album = AlbumStore.albummap.get(entry.type_src)
|
||||||
|
|
||||||
if album is None:
|
if album is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
album = album_serializer(
|
# album = album_serializer(
|
||||||
album,
|
# album,
|
||||||
to_remove={
|
# to_remove={
|
||||||
"genres",
|
# "genres",
|
||||||
"date",
|
# "date",
|
||||||
"count",
|
# "count",
|
||||||
"duration",
|
# "duration",
|
||||||
"albumartists_hashes",
|
# "albumartists_hashes",
|
||||||
"og_title",
|
# "og_title",
|
||||||
},
|
# },
|
||||||
)
|
# )
|
||||||
album["help_text"] = "album"
|
item = {
|
||||||
album["time"] = timestamp_to_time_passed(entry.timestamp)
|
"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",
|
||||||
"type": "album",
|
# "item": album,
|
||||||
"item": album,
|
# }
|
||||||
}
|
items.append(item)
|
||||||
)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if entry.type == "artist":
|
if entry.type == "artist":
|
||||||
artist = ArtistStore.get_artist_by_hash(entry.type_src)
|
artist = ArtistStore.artistmap.get(entry.type_src)
|
||||||
|
|
||||||
if artist is None:
|
if artist is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
artist = serialize_for_card(artist)
|
# artist = serialize_for_card(artist)
|
||||||
artist["help_text"] = "artist"
|
# artist["help_text"] = "artist"
|
||||||
artist["time"] = timestamp_to_time_passed(entry.timestamp)
|
# artist["time"] = timestamp_to_time_passed(entry.timestamp)
|
||||||
|
|
||||||
items.append(
|
items.append(
|
||||||
{
|
{
|
||||||
"type": "artist",
|
"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:
|
if is_home_dir:
|
||||||
folder = os.path.expanduser("~")
|
folder = os.path.expanduser("~")
|
||||||
|
|
||||||
# print(folder)
|
# count = FolderStore.count_tracks_containing_paths([folder])
|
||||||
# folder = os.path.join("/", folder, "")
|
item = {
|
||||||
# print(folder)
|
"type": "folder",
|
||||||
# count = len([t for t in TrackStore.tracks if t.folder == folder])
|
"hash": entry.type_src,
|
||||||
count = FolderStore.count_tracks_containing_paths([folder])
|
"timestamp": entry.timestamp,
|
||||||
items.append(
|
}
|
||||||
{
|
|
||||||
"type": "folder",
|
items.append(item)
|
||||||
"item": {
|
# {
|
||||||
"path": folder,
|
# "type": "folder",
|
||||||
"count": count[0]["trackcount"],
|
# "item": {
|
||||||
"help_text": "folder",
|
# "path": folder,
|
||||||
"time": timestamp_to_time_passed(entry.timestamp),
|
# "count": count[0]["trackcount"],
|
||||||
},
|
# "help_text": "folder",
|
||||||
}
|
# "time": timestamp_to_time_passed(entry.timestamp),
|
||||||
)
|
# },
|
||||||
|
# }
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if entry.type == "playlist":
|
if entry.type == "playlist":
|
||||||
is_custom = entry.type_src in [i["name"] for i in custom_playlists]
|
is_custom = entry.type_src in [i["name"] for i in custom_playlists]
|
||||||
# is_recently_added = entry.type_src == "recentlyadded"
|
|
||||||
|
|
||||||
if is_custom:
|
if is_custom:
|
||||||
playlist, _ = next(
|
# playlist, _ = next(
|
||||||
i["handler"]()
|
# i["handler"]()
|
||||||
for i in custom_playlists
|
# for i in custom_playlists
|
||||||
if i["name"] == entry.type_src
|
# if i["name"] == entry.type_src
|
||||||
)
|
# )
|
||||||
playlist.images = [i["image"] for i in playlist.images]
|
# playlist.images = [i["image"] for i in playlist.images]
|
||||||
|
|
||||||
playlist = serialize_playlist(
|
# playlist = serialize_playlist(
|
||||||
playlist, to_remove={"settings", "duration"}
|
# playlist, to_remove={"settings", "duration"}
|
||||||
)
|
# )
|
||||||
|
|
||||||
playlist["help_text"] = "playlist"
|
# playlist["help_text"] = "playlist"
|
||||||
playlist["time"] = timestamp_to_time_passed(entry.timestamp)
|
# playlist["time"] = timestamp_to_time_passed(entry.timestamp)
|
||||||
|
|
||||||
|
# items.append(
|
||||||
|
# {
|
||||||
|
# "type": "playlist",
|
||||||
|
# "item": playlist,
|
||||||
|
# }
|
||||||
|
# )
|
||||||
items.append(
|
items.append(
|
||||||
{
|
{
|
||||||
"type": "playlist",
|
"type": "playlist",
|
||||||
"item": playlist,
|
"hash": entry.type_src,
|
||||||
|
"timestamp": entry.timestamp,
|
||||||
|
"is_custom": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -152,34 +166,47 @@ def get_recently_played(limit=7):
|
|||||||
if playlist is None:
|
if playlist is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes)
|
item = {
|
||||||
playlist.clear_lists()
|
"type": "playlist",
|
||||||
|
"hash": entry.type_src,
|
||||||
|
"timestamp": entry.timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
if not playlist.has_image:
|
items.append(item)
|
||||||
images = get_first_4_images(tracks)
|
|
||||||
images = [i["image"] for i in images]
|
|
||||||
playlist.images = images
|
|
||||||
|
|
||||||
items.append(
|
# tracks = TrackStore.get_tracks_by_trackhashes(playlist.trackhashes)
|
||||||
{
|
# playlist.clear_lists()
|
||||||
"type": "playlist",
|
|
||||||
"item": {
|
# if not playlist.has_image:
|
||||||
"help_text": "playlist",
|
# images = get_first_4_images(tracks)
|
||||||
"time": timestamp_to_time_passed(entry.timestamp),
|
# images = [i["image"] for i in images]
|
||||||
**serialize_playlist(playlist),
|
# 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":
|
if entry.type == "favorite":
|
||||||
items.append(
|
items.append(
|
||||||
|
# {
|
||||||
|
# "type": "favorite_tracks",
|
||||||
|
# "item": {
|
||||||
|
# "help_text": "playlist",
|
||||||
|
# "count": FavoritesTable.count(),
|
||||||
|
# "time": timestamp_to_time_passed(entry.timestamp),
|
||||||
|
# },
|
||||||
|
# }
|
||||||
{
|
{
|
||||||
"type": "favorite_tracks",
|
"type": "favorite",
|
||||||
"item": {
|
"timestamp": entry.timestamp,
|
||||||
"help_text": "playlist",
|
|
||||||
"count": FavoritesTable.count(),
|
|
||||||
"time": timestamp_to_time_passed(entry.timestamp),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
@@ -189,22 +216,23 @@ def get_recently_played(limit=7):
|
|||||||
if t is None:
|
if t is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
track = serialize_track(t.get_best())
|
item = {
|
||||||
track["help_text"] = "track"
|
"type": "track",
|
||||||
track["time"] = timestamp_to_time_passed(entry.timestamp)
|
"hash": entry.trackhash,
|
||||||
|
"timestamp": entry.timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
items.append(
|
# track = serialize_track(t.get_best())
|
||||||
{
|
# track["help_text"] = "track"
|
||||||
"type": "track",
|
# track["time"] = timestamp_to_time_passed(entry.timestamp)
|
||||||
"item": track,
|
|
||||||
}
|
items.append(item)
|
||||||
)
|
|
||||||
|
|
||||||
BATCH_SIZE = 200
|
BATCH_SIZE = 200
|
||||||
current_index = 0
|
current_index = 0
|
||||||
|
|
||||||
entries = ScrobbleTable.get_all(0, BATCH_SIZE)
|
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
|
iterations = 0
|
||||||
|
|
||||||
while len(items) < limit and iterations < max_iterations:
|
while len(items) < limit and iterations < max_iterations:
|
||||||
@@ -212,7 +240,9 @@ def get_recently_played(limit=7):
|
|||||||
current_index += BATCH_SIZE
|
current_index += BATCH_SIZE
|
||||||
|
|
||||||
if len(items) < limit:
|
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:
|
if not entries:
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -226,6 +256,126 @@ def get_recently_played(limit=7):
|
|||||||
return items
|
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):
|
def get_recently_played_playlist(limit: int = 100):
|
||||||
playlist = Playlist(
|
playlist = Playlist(
|
||||||
id="recentlyplayed",
|
id="recentlyplayed",
|
||||||
|
|||||||
@@ -3,25 +3,13 @@ Recipes are a way to create mixes.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import Any, Dict, List
|
from typing import Any, 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
|
|
||||||
|
|
||||||
|
|
||||||
class HomepageRoutine(ABC):
|
class HomepageRoutine(ABC):
|
||||||
"""
|
"""
|
||||||
A routine creates a row of homepage items.
|
A routine creates a row of homepage items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
|
|
||||||
items: List[Mix]
|
|
||||||
extra: Dict[str, Any]
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def is_valid(self) -> bool: ...
|
def is_valid(self) -> bool: ...
|
||||||
@@ -30,37 +18,12 @@ class HomepageRoutine(ABC):
|
|||||||
if not self.is_valid:
|
if not self.is_valid:
|
||||||
return
|
return
|
||||||
|
|
||||||
self.items = self.run()
|
self.run()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def run(self) -> List[Mix]:
|
def run(self) -> List[Any]:
|
||||||
"""
|
"""
|
||||||
Creates the homepage items and saves them to the
|
Creates the homepage items and saves them to the
|
||||||
homepage store if self.is_valid is true.
|
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__()
|
|
||||||
|
|||||||
@@ -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__()
|
||||||
@@ -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
|
||||||
@@ -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 = {}
|
||||||
@@ -13,11 +13,17 @@ class TrackLog:
|
|||||||
duration: int
|
duration: int
|
||||||
timestamp: int
|
timestamp: int
|
||||||
source: str
|
source: str
|
||||||
|
"""
|
||||||
|
The full source string, eg. "al:123456"
|
||||||
|
"""
|
||||||
userid: int
|
userid: int
|
||||||
extra: dict[str, Any]
|
extra: dict[str, Any]
|
||||||
|
|
||||||
type = "track"
|
type = "track"
|
||||||
type_src = None
|
type_src = None
|
||||||
|
"""
|
||||||
|
The source identifier string, eg. albumhash, artisthash, etc.
|
||||||
|
"""
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
prefix_map = {
|
prefix_map = {
|
||||||
|
|||||||
+69
-23
@@ -1,11 +1,11 @@
|
|||||||
from abc import ABC
|
from abc import ABC
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from app.lib.home.recentlyplayed import recover_recently_played_items
|
||||||
from app.models.mix import Mix
|
from app.models.mix import Mix
|
||||||
from app.utils.auth import get_current_userid
|
from app.utils.auth import get_current_userid
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HomepageEntry(ABC):
|
class HomepageEntry(ABC):
|
||||||
"""
|
"""
|
||||||
Base class for all homepage entries.
|
Base class for all homepage entries.
|
||||||
@@ -15,20 +15,19 @@ class HomepageEntry(ABC):
|
|||||||
|
|
||||||
title: str
|
title: str
|
||||||
description: str
|
description: str
|
||||||
items: dict[int, dict[str, Any]]
|
items: dict[int, Any]
|
||||||
|
|
||||||
def __init__(self, title: str, description: str):
|
def __init__(self, title: str, description: str):
|
||||||
self.title = title
|
self.title = title
|
||||||
self.description = description
|
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.
|
Return usable items for the homepage.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MixHomepageEntry(HomepageEntry):
|
class MixHomepageEntry(HomepageEntry):
|
||||||
"""
|
"""
|
||||||
A homepage entry for mixes.
|
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:
|
class HomepageStore:
|
||||||
"""
|
"""
|
||||||
Stores the homepage items.
|
Stores the homepage items.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
entries = {
|
entries: dict[str, HomepageEntry] = {
|
||||||
|
"recently_played": RecentlyPlayedHomepageEntry(
|
||||||
|
title="Recently played",
|
||||||
|
),
|
||||||
"artist_mixes": MixHomepageEntry(
|
"artist_mixes": MixHomepageEntry(
|
||||||
title="Artist mixes for you",
|
title="Artist mixes for you",
|
||||||
description="Based on artists you have been listening to",
|
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
|
@classmethod
|
||||||
def set_mixes(cls, mixes: list[Mix], mixkey: str, userid: int | None = None):
|
def set_mixes(cls, items: list[Any], entrykey: str, userid: int | None = None):
|
||||||
idmap = {mix.id[1:]: mix for mix in mixes}
|
idmap = {item.id[1:]: item for item in items}
|
||||||
cls.entries[mixkey].items[userid or get_current_userid()] = idmap
|
cls.entries[entrykey].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)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_mix(cls, mixkey: str, mixid: str):
|
def get_mix(cls, mixkey: str, mixid: str):
|
||||||
@@ -89,14 +139,10 @@ class HomepageStore:
|
|||||||
return mix.to_full_dict() if mix else None
|
return mix.to_full_dict() if mix else None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_mix_by_sourcehash(cls, sourcehash: str):
|
def get_homepage_items(cls, limit: int):
|
||||||
return next(
|
# return a dict of entry name to entry items
|
||||||
(
|
return [
|
||||||
mix
|
{entry: cls.entries[entry].get_items(get_current_userid(), limit)}
|
||||||
for mix in cls.entries["artist_mixes"]
|
for entry in cls.entries.keys()
|
||||||
.items.get(get_current_userid(), {})
|
if len(cls.entries[entry].items)
|
||||||
.values()
|
]
|
||||||
if mix.sourcehash == sourcehash
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
|
|||||||
+1
-1
@@ -46,7 +46,7 @@ def date_string_to_time_passed(prev_date: str) -> str:
|
|||||||
return timestamp_to_time_passed(then)
|
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.
|
Converts seconds to a time string. e.g. 1 hour 2 minutes, 1 hour 2 seconds, 1 hour, 1 minute 2 seconds, etc.
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user