A show folder under Kinderpodcasts/ opts in by containing a feed.txt marker naming its RSS feed. A new PodcastFeedService polls every such feed every 6 hours (and once at startup), downloads any episode not already on disk using the existing YYYYMMDD - Title.ext convention, and triggers the same rescan-and-broadcast sequence "Bibliothek neu einlesen" already uses - now shared via App.rescan_library() instead of duplicated. A show with no feed.txt is untouched, so there is no new config section for this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
68 lines
2.2 KiB
Python
68 lines
2.2 KiB
Python
"""What the reactions get handed: the three objects, the bus, and a little shared state."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass, field
|
|
|
|
from musicmouse.bus import EventBus
|
|
from musicmouse.clock import Clock, RealClock
|
|
from musicmouse.config import Config, FigureColors
|
|
from musicmouse.devices.mouse import MusicMouseDevice
|
|
from musicmouse.devices.player import Player
|
|
from musicmouse.library import MusicLibrary
|
|
from musicmouse.media import Playlist
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
__all__ = ["App", "AppState"]
|
|
|
|
|
|
@dataclass
|
|
class AppState:
|
|
"""State that belongs to no single device but is shared between reactions."""
|
|
|
|
#: Figure that was taken off the reader mid-playlist, so putting it back resumes
|
|
#: instead of starting over. Cleared once its playlist runs out.
|
|
last_partially_played_figure: str | None = None
|
|
|
|
#: Whether the MQTT broker is currently reachable. The firmware's equivalent is
|
|
#: readable off the transport; a broker's is not, so it is remembered here.
|
|
mqtt_connected: bool = False
|
|
|
|
|
|
@dataclass
|
|
class App:
|
|
config: Config
|
|
bus: EventBus
|
|
mouse: MusicMouseDevice
|
|
player: Player
|
|
library: MusicLibrary
|
|
playlists: dict[str, Playlist]
|
|
clock: Clock = field(default_factory=RealClock)
|
|
state: AppState = field(default_factory=AppState)
|
|
|
|
def colors(self, figure: str) -> FigureColors:
|
|
return self.config.figures[figure].colors
|
|
|
|
def playlist(self, figure: str) -> Playlist | None:
|
|
playlist = self.playlists.get(figure)
|
|
if playlist is None:
|
|
_log.warning("No playlist for figure %r", figure)
|
|
return playlist
|
|
|
|
async def rescan_library(
|
|
self, *, broadcast: Callable[[], Awaitable[None]] | None = None
|
|
) -> None:
|
|
"""Rescan from disk, rebuild figure playlists, and tell whoever's listening.
|
|
|
|
Shared by the manual "Bibliothek neu einlesen" endpoint and anything else that
|
|
can change what's on disk on its own, such as the podcast feed poller.
|
|
"""
|
|
await self.library.refresh()
|
|
self.playlists.clear()
|
|
self.playlists.update(self.library.figure_playlists())
|
|
if broadcast is not None:
|
|
await broadcast()
|