Auto-download new podcast episodes from an RSS feed
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>
This commit is contained in:
60
python-backend/musicmouse/services/podcasts.py
Normal file
60
python-backend/musicmouse/services/podcasts.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Front-end for nothing: the only "intent" this service ever produces is new files on
|
||||
disk. It polls every podcast show that named its feed via a ``feed.txt`` marker (see
|
||||
:mod:`musicmouse.library.podcast_feeds`) and downloads whatever episode is missing.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Final
|
||||
|
||||
import httpx2
|
||||
|
||||
from musicmouse.library import MusicLibrary
|
||||
from musicmouse.library.podcast_feeds import sync_all_shows
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
__all__ = ["PodcastFeedService"]
|
||||
|
||||
#: Podcasts a children's player subscribes to publish at most a few times a week;
|
||||
#: checking a few times a day is plenty and kind to the feed hosts.
|
||||
_CHECK_INTERVAL_SECONDS: Final = 6 * 3600
|
||||
|
||||
|
||||
class PodcastFeedService:
|
||||
name = "podcast-feeds"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
library: MusicLibrary,
|
||||
*,
|
||||
client: httpx2.AsyncClient,
|
||||
on_change: Callable[[], Awaitable[None]],
|
||||
interval: float = _CHECK_INTERVAL_SECONDS,
|
||||
) -> None:
|
||||
self.library = library
|
||||
self.client = client
|
||||
self.on_change = on_change
|
||||
self.interval = interval
|
||||
|
||||
async def run(self) -> None:
|
||||
"""Checks immediately at startup, then every `interval` seconds.
|
||||
|
||||
A no-op when no show has a ``feed.txt`` - which is the common case, so this can
|
||||
run unconditionally instead of needing its own config toggle. Never lets a
|
||||
single bad pass end the task: `Service.run` is cancelled on shutdown and is
|
||||
otherwise expected to keep going on its own.
|
||||
"""
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
if await sync_all_shows(self.client, self.library.root):
|
||||
await self.on_change()
|
||||
except Exception:
|
||||
_log.exception("Podcast feed check failed; will retry next interval")
|
||||
await asyncio.sleep(self.interval)
|
||||
finally:
|
||||
await self.client.aclose()
|
||||
@@ -100,14 +100,8 @@ def build_router(
|
||||
|
||||
@router.post("/library/refresh", status_code=202)
|
||||
async def refresh_library() -> Response:
|
||||
async def rescan() -> None:
|
||||
await app.library.refresh()
|
||||
app.playlists.clear()
|
||||
app.playlists.update(app.library.figure_playlists())
|
||||
await hub.broadcast_library()
|
||||
|
||||
# Returns immediately: a cold rescan reads tags from every file.
|
||||
asyncio.create_task(rescan()) # noqa: RUF006
|
||||
asyncio.create_task(app.rescan_library(broadcast=hub.broadcast_library)) # noqa: RUF006
|
||||
return Response(status_code=202)
|
||||
|
||||
# --------------------------------------------------------------------- state
|
||||
|
||||
Reference in New Issue
Block a user