mirror of https://github.com/Nezreka/SoulSync.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
122 lines
4.0 KiB
122 lines
4.0 KiB
"""Per-kind specifications for the personalized-playlist subsystem.
|
|
|
|
A ``PlaylistKindSpec`` declares everything the manager needs to know
|
|
about one playlist type:
|
|
|
|
- The kind identifier (stable string used in URLs / configs / DB).
|
|
- Human-readable display name template (with optional ``{variant}``
|
|
substitution).
|
|
- Whether the kind supports / requires variants and what valid
|
|
variants look like.
|
|
- The default user-tweakable config for this kind.
|
|
- A generator callable that produces a fresh track list given
|
|
``(deps, variant, config)``.
|
|
|
|
Generators live in ``core/personalized/generators/`` (added in
|
|
later commits as each kind is migrated). For commit 1 the registry
|
|
ships empty — schema + manager land first; generators arrive
|
|
incrementally with their per-kind tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Callable, Dict, List, Optional
|
|
|
|
from core.personalized.types import PlaylistConfig, Track
|
|
|
|
|
|
# Generator callable signature.
|
|
# Args:
|
|
# deps: opaque object the generator may need (DB, service handle,
|
|
# config_manager). Each generator declares what it pulls.
|
|
# variant: e.g. '1980s' for time machine, 'halloween' for seasonal.
|
|
# Empty string '' for singleton kinds.
|
|
# config: PlaylistConfig with the user's per-playlist overrides
|
|
# merged onto the kind's defaults.
|
|
# Returns:
|
|
# List[Track] — the fresh snapshot to persist as the playlist's
|
|
# current track list.
|
|
GeneratorFn = Callable[[Any, str, PlaylistConfig], List[Track]]
|
|
|
|
|
|
# Variant resolver: returns the list of currently-valid variant
|
|
# identifiers for a kind that supports multiple instances. Used by
|
|
# the manager when auto-creating playlist rows for newly available
|
|
# variants (e.g. a new decade, a new season). Singletons return [''].
|
|
VariantResolver = Callable[[Any], List[str]]
|
|
|
|
|
|
@dataclass
|
|
class PlaylistKindSpec:
|
|
"""Declaration of one playlist kind.
|
|
|
|
See module docstring for the contract.
|
|
"""
|
|
|
|
kind: str
|
|
name_template: str # e.g. 'Time Machine — {variant}', 'Hidden Gems'
|
|
description: str
|
|
default_config: PlaylistConfig
|
|
generator: GeneratorFn
|
|
variant_resolver: Optional[VariantResolver] = None
|
|
requires_variant: bool = False
|
|
# Tags for UI grouping ('curated' / 'discovery' / 'time' / 'genre').
|
|
tags: List[str] = field(default_factory=list)
|
|
|
|
def display_name(self, variant: str) -> str:
|
|
"""Render the human-readable playlist name for a given variant."""
|
|
if not variant:
|
|
return self.name_template.replace('{variant}', '').strip(' —-')
|
|
return self.name_template.format(variant=variant)
|
|
|
|
|
|
class PlaylistKindRegistry:
|
|
"""Module-level registry of every kind the manager knows about.
|
|
|
|
Populated at import time as each generator module is loaded. The
|
|
manager queries the registry at runtime to dispatch refresh
|
|
requests, list available kinds for the UI, and resolve variants.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
self._kinds: Dict[str, PlaylistKindSpec] = {}
|
|
|
|
def register(self, spec: PlaylistKindSpec) -> None:
|
|
if spec.kind in self._kinds:
|
|
raise ValueError(f"Kind {spec.kind!r} already registered")
|
|
self._kinds[spec.kind] = spec
|
|
|
|
def get(self, kind: str) -> Optional[PlaylistKindSpec]:
|
|
return self._kinds.get(kind)
|
|
|
|
def all(self) -> List[PlaylistKindSpec]:
|
|
return list(self._kinds.values())
|
|
|
|
def kinds(self) -> List[str]:
|
|
return list(self._kinds.keys())
|
|
|
|
def reset_for_tests(self) -> None:
|
|
"""Drop every registration. Tests only — production runs
|
|
register at module import and never reset."""
|
|
self._kinds.clear()
|
|
|
|
|
|
# Module-level singleton. Generators register against this on import.
|
|
_registry = PlaylistKindRegistry()
|
|
|
|
|
|
def get_registry() -> PlaylistKindRegistry:
|
|
"""Public accessor for the module-level registry. Tests can reset
|
|
via ``get_registry().reset_for_tests()``."""
|
|
return _registry
|
|
|
|
|
|
__all__ = [
|
|
'PlaylistKindSpec',
|
|
'PlaylistKindRegistry',
|
|
'GeneratorFn',
|
|
'VariantResolver',
|
|
'get_registry',
|
|
]
|