fix(podcasts): generic media delete refuses episode rows (invariant 2)
The admin endpoint DELETE /api/media-items/:id predated podcasts and would delete any media row — including podcast episodes — if hit directly. Invariant 2 (absolute form, user decision 2026-10-30) says episode rows are removed ONLY by whole-feed removal: "the feed should match the XML until the entire feed is gone." The plan audit found this endpoint was the one remaining surface that could violate it: the poller, unsubscribe, and the admin file-deletion endpoints are files-only, and the hash-conflict merge can't see episodes (they never hash). The guard sits right after the GetMediaItem read and BEFORE the storage-down check, so a refused episode never touches storage resolution. Same segregation pattern as the generic edit path (D9/D10, UpdateMediaItem). Pinned by TestDeleteMediaItemPodcastGuard: admin DELETE of an episode → 409 with the row surviving; a book delete through the same endpoint → 204 control.
This commit is contained in:
1 parent
5fee8bfb4a
commit
f47734dd7d
2 files changed
+59
No files matched your search
@@ -1881,6 +1881,17 @@ func (mh *MediaHandler) DeleteMediaItem(c *echo.Context) error {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
// Invariant 2 (podcast plan §10, absolute form): episode rows are
|
||||
// removed ONLY when the whole feed is removed — the feed must match
|
||||
// its XML until then. Same segregation as the edit path (D9/D10):
|
||||
// episodes are fetcher-owned; delete FILES via the podcast surfaces
|
||||
// (feed page / admin podcast view) instead.
|
||||
if media.Source == "podcast_episode" {
|
||||
return c.JSON(http.StatusConflict, map[string]string{
|
||||
"error": "podcast episodes are removed only by removing the whole feed — delete the episode's files instead (feed page or admin podcast view)",
|
||||
})
|
||||
}
|
||||
|
||||
// Storage-down guard (see HandleBulkDelete): refuse while the library
|
||||
// storage is unreachable so the cascade can't destroy annotations for
|
||||
// a file that will re-import on the next scan.
|
||||
|
||||
Reference in new issue
Block a user