Support MPD_NOW_PLAYABLE_CACHE setting (Redis or Memcached)
This commit is contained in:
parent
c7773bf324
commit
55d82c72c3
7 changed files with 118 additions and 36 deletions
55
src/mpd_now_playable/cache.py
Normal file
55
src/mpd_now_playable/cache.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import suppress
|
||||
from typing import Any, Optional, TypeVar
|
||||
from urllib.parse import parse_qsl, urlparse
|
||||
|
||||
from aiocache import Cache
|
||||
from aiocache.serializers import BaseSerializer, PickleSerializer
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
HAS_ORMSGPACK = False
|
||||
with suppress(ImportError):
|
||||
import ormsgpack
|
||||
|
||||
HAS_ORMSGPACK = True
|
||||
|
||||
|
||||
class OrmsgpackSerializer(BaseSerializer):
|
||||
DEFAULT_ENCODING = None
|
||||
|
||||
def dumps(self, value: Any) -> bytes:
|
||||
return ormsgpack.packb(value)
|
||||
|
||||
def loads(self, value: Optional[bytes]) -> Any:
|
||||
if value is None:
|
||||
return None
|
||||
return ormsgpack.unpackb(value)
|
||||
|
||||
|
||||
def make_cache(url: str, namespace: str = "") -> Cache[T]:
|
||||
parsed_url = urlparse(url)
|
||||
backend = Cache.get_scheme_class(parsed_url.scheme)
|
||||
if backend == Cache.MEMORY:
|
||||
return Cache(backend)
|
||||
kwargs: dict[str, Any] = dict(parse_qsl(parsed_url.query))
|
||||
|
||||
if parsed_url.path:
|
||||
kwargs.update(backend.parse_uri_path(parsed_url.path))
|
||||
|
||||
if parsed_url.hostname:
|
||||
kwargs["endpoint"] = parsed_url.hostname
|
||||
|
||||
if parsed_url.port:
|
||||
kwargs["port"] = parsed_url.port
|
||||
|
||||
if parsed_url.password:
|
||||
kwargs["password"] = parsed_url.password
|
||||
|
||||
namespace = ":".join(s for s in [kwargs.get("namespace"), namespace] if s)
|
||||
del kwargs["namespace"]
|
||||
|
||||
serializer = OrmsgpackSerializer if HAS_ORMSGPACK else PickleSerializer
|
||||
|
||||
return Cache(backend, serializer=serializer(), namespace=namespace, **kwargs)
|
|
@ -11,10 +11,11 @@ async def listen() -> None:
|
|||
port = int(environ.get("MPD_PORT", "6600"))
|
||||
host = environ.get("MPD_HOST", "localhost")
|
||||
password = environ.get("MPD_PASSWORD")
|
||||
cache = environ.get("MPD_NOW_PLAYABLE_CACHE")
|
||||
if password is None and "@" in host:
|
||||
password, host = host.split("@", maxsplit=1)
|
||||
|
||||
listener = MpdStateListener()
|
||||
listener = MpdStateListener(cache)
|
||||
now_playing = CocoaNowPlaying(listener)
|
||||
await listener.start(host=host, port=port, password=password)
|
||||
await listener.loop(now_playing)
|
||||
|
|
|
@ -1,30 +1,16 @@
|
|||
from dataclasses import dataclass
|
||||
from __future__ import annotations
|
||||
|
||||
from aiocache import Cache
|
||||
from typing import TypedDict
|
||||
|
||||
from ..async_tools import run_background_task
|
||||
from ..cache import Cache, make_cache
|
||||
from .types import CurrentSongResponse, MpdStateHandler
|
||||
|
||||
CACHE_TTL = 60 * 10 # ten minutes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HasArt:
|
||||
data: bytes
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HasNoArt:
|
||||
data = None
|
||||
|
||||
|
||||
ArtCacheEntry = HasArt | HasNoArt
|
||||
|
||||
|
||||
def make_cache_entry(art: bytes | None) -> ArtCacheEntry:
|
||||
if art is None:
|
||||
return HasNoArt()
|
||||
return HasArt(art)
|
||||
class ArtCacheEntry(TypedDict):
|
||||
data: bytes | None
|
||||
|
||||
|
||||
def calc_album_key(song: CurrentSongResponse) -> str:
|
||||
|
@ -39,18 +25,18 @@ def calc_track_key(song: CurrentSongResponse) -> str:
|
|||
|
||||
class MpdArtworkCache:
|
||||
mpd: MpdStateHandler
|
||||
album_cache: "Cache[ArtCacheEntry]"
|
||||
track_cache: "Cache[ArtCacheEntry]"
|
||||
album_cache: Cache[ArtCacheEntry]
|
||||
track_cache: Cache[ArtCacheEntry]
|
||||
|
||||
def __init__(self, mpd: MpdStateHandler):
|
||||
def __init__(self, mpd: MpdStateHandler, cache_url: str = "memory://"):
|
||||
self.mpd = mpd
|
||||
self.album_cache = Cache()
|
||||
self.track_cache = Cache()
|
||||
self.album_cache = make_cache(cache_url, "album")
|
||||
self.track_cache = make_cache(cache_url, "track")
|
||||
|
||||
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
|
||||
art = await self.track_cache.get(calc_track_key(song))
|
||||
if art:
|
||||
return art.data
|
||||
return art["data"]
|
||||
|
||||
# If we don't have track artwork cached, go find some.
|
||||
run_background_task(self.cache_artwork(song))
|
||||
|
@ -58,12 +44,12 @@ class MpdArtworkCache:
|
|||
# Even if we don't have cached track art, we can try looking for cached album art.
|
||||
art = await self.album_cache.get(calc_album_key(song))
|
||||
if art:
|
||||
return art.data
|
||||
return art["data"]
|
||||
|
||||
return None
|
||||
|
||||
async def cache_artwork(self, song: CurrentSongResponse) -> None:
|
||||
art = make_cache_entry(await self.mpd.readpicture(song["file"]))
|
||||
art = ArtCacheEntry(data=await self.mpd.readpicture(song["file"]))
|
||||
try:
|
||||
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
|
||||
except ValueError:
|
||||
|
|
|
@ -44,9 +44,11 @@ class MpdStateListener(Player):
|
|||
art_cache: MpdArtworkCache
|
||||
idle_count = 0
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(self, cache: str | None = None) -> None:
|
||||
self.client = MPDClient()
|
||||
self.art_cache = MpdArtworkCache(self)
|
||||
self.art_cache = (
|
||||
MpdArtworkCache(self, cache) if cache else MpdArtworkCache(self)
|
||||
)
|
||||
|
||||
async def start(
|
||||
self, host: str = "localhost", port: int = 6600, password: str | None = None
|
||||
|
|
7
stubs/aiocache/base.pyi
Normal file
7
stubs/aiocache/base.pyi
Normal file
|
@ -0,0 +1,7 @@
|
|||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class BaseCache(Generic[T]):
|
||||
@staticmethod
|
||||
def parse_uri_path(path: str) -> dict[str, str]: ...
|
|
@ -1,8 +1,25 @@
|
|||
from typing import Generic, Optional, TypeVar
|
||||
from typing import ClassVar, Optional, TypeVar
|
||||
|
||||
T = TypeVar('T')
|
||||
from .base import BaseCache
|
||||
from .serializers import BaseSerializer
|
||||
|
||||
class Cache(Generic[T]):
|
||||
async def add(self, key: str, value: T, ttl: Optional[int]) -> None: ...
|
||||
async def set(self, key: str, value: T, ttl: Optional[int]) -> None: ... # noqa: A003
|
||||
async def get(self, key: str, default: T | None = None) -> T | None: ...
|
||||
T = TypeVar("T")
|
||||
|
||||
class Cache(BaseCache[T]):
|
||||
MEMORY: ClassVar[type[BaseCache]]
|
||||
REDIS: ClassVar[type[BaseCache] | None]
|
||||
MEMCACHED: ClassVar[type[BaseCache] | None]
|
||||
|
||||
def __new__(
|
||||
cls,
|
||||
cache_class: type[BaseCache] = MEMORY,
|
||||
*,
|
||||
serializer: Optional[BaseSerializer] = None,
|
||||
namespace: str = "",
|
||||
**kwargs,
|
||||
) -> Cache[T]: ...
|
||||
@staticmethod
|
||||
def get_scheme_class(scheme: str) -> type[BaseCache]: ...
|
||||
async def add(self, key: str, value: T, ttl: Optional[int]) -> None: ...
|
||||
async def set(self, key: str, value: T, ttl: Optional[int]) -> None: ... # noqa: A003
|
||||
async def get(self, key: str, default: T | None = None) -> T | None: ...
|
||||
|
|
14
stubs/aiocache/serializers.pyi
Normal file
14
stubs/aiocache/serializers.pyi
Normal file
|
@ -0,0 +1,14 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Optional
|
||||
|
||||
class BaseSerializer(ABC):
|
||||
DEFAULT_ENCODING: Optional[str] = "utf-8"
|
||||
@abstractmethod
|
||||
def dumps(self, value: Any, /) -> Any: ...
|
||||
@abstractmethod
|
||||
def loads(self, value: Any, /) -> Any: ...
|
||||
|
||||
class PickleSerializer(BaseSerializer):
|
||||
DEFAULT_ENCODING = None
|
||||
def dumps(self, value: Any, /) -> Any: ...
|
||||
def loads(self, value: Any, /) -> Any: ...
|
Loading…
Reference in a new issue