[generators] Default per artist up to 4
Some checks failed
CI / Build (push) Has been cancelled
CI / pyright (push) Has been cancelled
CI / pytest (3.13) (push) Has been cancelled
CI / ruff check (push) Has been cancelled
CI / ruff format (push) Has been cancelled
CI / pytest (3.14) (push) Has been cancelled

This commit is contained in:
2026-06-24 22:38:42 -04:00
parent b56ab3fe65
commit 392188b560
4 changed files with 53 additions and 24 deletions

View File

@ -25,6 +25,7 @@ class Extension(ext.Extension):
schema["playlist_dir"] = config.String(optional=True)
schema["max_tracks"] = config.Integer(optional=True, minimum=0)
schema["max_per_album"] = config.Integer(optional=True, minimum=0)
schema["max_per_artist"] = config.Integer(optional=True, minimum=0)
return schema
def validate_environment(self) -> None:

View File

@ -26,3 +26,5 @@ playlist_dir =
max_tracks = 75
# Maximum tracks per album in a mix (0 = no limit)
max_per_album = 2
# Maximum tracks per artist in a mix (0 = no limit)
max_per_artist = 4

View File

@ -34,22 +34,38 @@ def _search(
def _mix_tracks(
tracks: list[Track], max_tracks: int = 0, max_per_album: int = 0,
tracks: list[Track], max_tracks: int = 0,
max_per_album: int = 0, max_per_artist: int = 0,
) -> list[Track]:
if not tracks:
return []
if max_per_album > 0:
random.shuffle(tracks)
seen: dict[str, int] = {}
result: list[Track] = []
for t in tracks:
key = t.album.uri if t.album and t.album.uri else str(id(t))
count = seen.get(key, 0)
if count >= max_per_album:
random.shuffle(tracks)
seen_album: dict[str, int] = {}
seen_artist: dict[str, int] = {}
result: list[Track] = []
for t in tracks:
if max_per_album > 0:
album_key = t.album.uri if t.album and t.album.uri else str(id(t))
if seen_album.get(album_key, 0) >= max_per_album:
continue
seen[key] = count + 1
result.append(t)
tracks = result
if max_per_artist > 0:
artists = t.artists or []
artist_key = None
for a in artists:
if a.uri:
artist_key = a.uri
break
if not artist_key:
artist_name = (artists[0].name or "") if artists else ""
artist_key = artist_name or str(id(t))
if seen_artist.get(artist_key, 0) >= max_per_artist:
continue
if max_per_album > 0:
seen_album[album_key] = seen_album.get(album_key, 0) + 1
if max_per_artist > 0:
seen_artist[artist_key] = seen_artist.get(artist_key, 0) + 1
result.append(t)
tracks = result
if max_tracks > 0:
tracks = tracks[:max_tracks]
random.shuffle(tracks)
@ -58,7 +74,7 @@ def _mix_tracks(
def build_decade_mix(
core: CoreProxy, decade: str, uris: list[Uri] | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> list[Track]:
query = cast("Query[SearchField]", {"date": [parse_decade(decade)]})
try:
@ -66,23 +82,23 @@ def build_decade_mix(
except Exception:
logger.exception("Decade search failed for %s", decade)
return []
tracks = _mix_tracks(_extract_tracks(result), max_tracks, max_per_album)
tracks = _mix_tracks(_extract_tracks(result), max_tracks, max_per_album, max_per_artist)
logger.info("Found %d tracks for decade %s", len(tracks), decade)
return tracks
def build_genre_mix(
core: CoreProxy, genre: str, uris: list[Uri] | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> list[Track]:
return _mix_tracks(_search(core, "genre", genre, uris=uris), max_tracks, max_per_album)
return _mix_tracks(_search(core, "genre", genre, uris=uris), max_tracks, max_per_album, max_per_artist)
def build_artist_mix(
core: CoreProxy, artist: str, uris: list[Uri] | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> list[Track]:
return _mix_tracks(_search(core, "artist", artist, uris=uris), max_tracks, max_per_album)
return _mix_tracks(_search(core, "artist", artist, uris=uris), max_tracks, max_per_album, max_per_artist)
def build_album_mix(core: CoreProxy, album_uri: str) -> list[Track]:

View File

@ -29,7 +29,7 @@ class DecadeMixHandler(tornado.web.RequestHandler):
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
playlist_dir: str | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> None:
self.core = core
self.prefix = prefix
@ -37,6 +37,7 @@ class DecadeMixHandler(tornado.web.RequestHandler):
self.playlist_dir = playlist_dir
self.max_tracks = max_tracks
self.max_per_album = max_per_album
self.max_per_artist = max_per_artist
def post(self) -> None:
data = json.loads(self.request.body)
@ -48,6 +49,7 @@ class DecadeMixHandler(tornado.web.RequestHandler):
tracks = build_decade_mix(
self.core, decade, uris=self.uris,
max_tracks=self.max_tracks, max_per_album=self.max_per_album,
max_per_artist=self.max_per_artist,
)
if not tracks:
self.write({"playlist": None, "tracks": 0})
@ -71,7 +73,7 @@ class GenreMixHandler(tornado.web.RequestHandler):
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
playlist_dir: str | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> None:
self.core = core
self.prefix = prefix
@ -79,6 +81,7 @@ class GenreMixHandler(tornado.web.RequestHandler):
self.playlist_dir = playlist_dir
self.max_tracks = max_tracks
self.max_per_album = max_per_album
self.max_per_artist = max_per_artist
def post(self) -> None:
data = json.loads(self.request.body)
@ -90,6 +93,7 @@ class GenreMixHandler(tornado.web.RequestHandler):
tracks = build_genre_mix(
self.core, genre, uris=self.uris,
max_tracks=self.max_tracks, max_per_album=self.max_per_album,
max_per_artist=self.max_per_artist,
)
if not tracks:
self.write({"playlist": None, "tracks": 0})
@ -113,7 +117,7 @@ class ArtistMixHandler(tornado.web.RequestHandler):
def initialize(
self, core: CoreProxy, prefix: str, uris: list[Uri] | None = None,
playlist_dir: str | None = None,
max_tracks: int = 0, max_per_album: int = 0,
max_tracks: int = 0, max_per_album: int = 0, max_per_artist: int = 0,
) -> None:
self.core = core
self.prefix = prefix
@ -121,6 +125,7 @@ class ArtistMixHandler(tornado.web.RequestHandler):
self.playlist_dir = playlist_dir
self.max_tracks = max_tracks
self.max_per_album = max_per_album
self.max_per_artist = max_per_artist
def post(self) -> None:
data = json.loads(self.request.body)
@ -132,6 +137,7 @@ class ArtistMixHandler(tornado.web.RequestHandler):
tracks = build_artist_mix(
self.core, artist, uris=self.uris,
max_tracks=self.max_tracks, max_per_album=self.max_per_album,
max_per_artist=self.max_per_artist,
)
if not tracks:
self.write({"playlist": None, "tracks": 0})
@ -323,17 +329,21 @@ def app_factory(config: Config, core: CoreProxy) -> list[tuple]:
playlist_dir = _parse_playlist_dir(config)
max_tracks = _parse_int(config, "max_tracks", 0)
max_per_album = _parse_int(config, "max_per_album", 0)
max_per_artist = _parse_int(config, "max_per_artist", 0)
return [
(r"/decade", DecadeMixHandler,
{"core": core, "prefix": prefix, "uris": uris, "playlist_dir": playlist_dir,
"max_tracks": max_tracks, "max_per_album": max_per_album}),
"max_tracks": max_tracks, "max_per_album": max_per_album,
"max_per_artist": max_per_artist}),
(r"/genre", GenreMixHandler,
{"core": core, "prefix": prefix, "uris": uris, "playlist_dir": playlist_dir,
"max_tracks": max_tracks, "max_per_album": max_per_album}),
"max_tracks": max_tracks, "max_per_album": max_per_album,
"max_per_artist": max_per_artist}),
(r"/artist", ArtistMixHandler,
{"core": core, "prefix": prefix, "uris": uris, "playlist_dir": playlist_dir,
"max_tracks": max_tracks, "max_per_album": max_per_album}),
"max_tracks": max_tracks, "max_per_album": max_per_album,
"max_per_artist": max_per_artist}),
(r"/album", AlbumMixHandler,
{"core": core, "prefix": prefix, "playlist_dir": playlist_dir}),
(r"/instant-mix", InstantMixHandler,