Keep only the newest 50 episodes of each podcast

A show that has published for years is unbounded: GEOlino Spezial alone is 358
episodes and 5.8 GB, and the device it syncs onto is a 30 GB SD card that also
holds the rest of the library. Nothing stopped the 6-hourly poll from eventually
filling it.

Cap each show's folder at general.podcast_episode_limit (default 50, null to
keep everything), pruning the oldest past that after each sync pass.

The same limit caps what is downloaded, and it has to be one number for both.
Prune to the newest N but keep fetching everything the feed offers, and every
poll would re-download exactly the episodes the previous one deleted - forever,
at full size, since missing_episodes() decides purely from what is on disk.
There is a test for that specific loop.

Pruning only touches files named the way this module names them
(YYYYMMDD - Title.ext), so feed.txt, folder.jpg, the failed-download record and
anything placed by hand are all left alone; a parse that fails means "not ours",
not "delete it". An episode's sidecar cover goes with it. A pass that only
deleted still reports a change, because the library needs the rescan just as
much as it does after a download.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-19 20:40:53 +02:00
parent dd14f5d901
commit fa3d92189c
8 changed files with 307 additions and 14 deletions

View File

@@ -13,7 +13,7 @@ from typing import Final
import httpx2
from musicmouse.library import MusicLibrary
from musicmouse.library.podcast_feeds import sync_all_shows
from musicmouse.library.podcast_feeds import DEFAULT_EPISODE_LIMIT, sync_all_shows
_log = logging.getLogger(__name__)
@@ -34,11 +34,13 @@ class PodcastFeedService:
client: httpx2.AsyncClient,
on_change: Callable[[], Awaitable[None]],
interval: float = _CHECK_INTERVAL_SECONDS,
episode_limit: int | None = DEFAULT_EPISODE_LIMIT,
) -> None:
self.library = library
self.client = client
self.on_change = on_change
self.interval = interval
self.episode_limit = episode_limit
async def run(self) -> None:
"""Checks immediately at startup, then every `interval` seconds.
@@ -51,7 +53,8 @@ class PodcastFeedService:
try:
while True:
try:
if await sync_all_shows(self.client, self.library.root):
keep = self.episode_limit
if await sync_all_shows(self.client, self.library.root, keep=keep):
await self.on_change()
except Exception:
_log.exception("Podcast feed check failed; will retry next interval")