From 54d7d6e7a1d7e0d81c9249760a13da8fab8532d7 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 29 Oct 2026 16:05:03 -0400 Subject: [PATCH] =?UTF-8?q?feat(podcasts):=20edit=20segregation=20?= =?UTF-8?q?=E2=80=94=20feed=20metadata=20is=20fetcher-owned=20(task=201.8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit D9/D10: users never edit podcast episode metadata. Three gates: - Generic edit endpoint (UpdateMediaItem) rejects source= 'podcast_episode' items with 400 — the next fetch refresh would clobber any edit anyway, and the fetcher is the sole metadata writer. - Write-to-file (MetadataWriter.WriteMetadataToFile) refuses podcast episodes for the same reason (tag write-back for downloaded episodes is a named Phase-4 item needing its own iTunes-tag mapping). - The book-detail page hides the admin Edit affordance for podcasts-type items (direct-URL visits), and the metadata-editor modal's field list has no podcast fields by construction (fixed templ field list) — episodes render read-only via /podcasts surfaces. --- internal/handlers/media.go | 7 +++++++ internal/services/metadata_writer.go | 8 ++++++++ templates/book_detail.templ | 2 +- templates/book_detail_templ.go | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/handlers/media.go b/internal/handlers/media.go index f61b85ed..cf8606d2 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -1542,6 +1542,13 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error { return c.JSON(http.StatusNotFound, map[string]string{"error": "media item not found"}) } + // Edit segregation (plan D10, task 1.8): podcast episodes are + // fetcher-owned (D9 — the sole writer); the generic edit box would + // clobber feed metadata that the next refresh overwrites anyway. + if existing.Source == "podcast_episode" { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "podcast episodes are managed by their feed and cannot be edited here"}) + } + coverPath := existing.CoverImagePath.String coverUploaded := false diff --git a/internal/services/metadata_writer.go b/internal/services/metadata_writer.go index ede392d2..3fef039c 100644 --- a/internal/services/metadata_writer.go +++ b/internal/services/metadata_writer.go @@ -110,6 +110,14 @@ func (w *MetadataWriter) WriteMetadataToFile(ctx context.Context, itemID uuid.UU return nil, fmt.Errorf("media item not found: %w", err) } + // Edit segregation (plan D10/D9, task 1.8): podcast episodes are + // feed-managed; writing their metadata into the file would be + // clobbered by the next download/refresh and violates the + // fetcher's sole-writer rule. + if item.Source == "podcast_episode" { + return nil, fmt.Errorf("podcast episodes are managed by their feed; metadata write-to-file is not available") + } + absPath, err := w.libraries.ResolveMediaPath(ctx, item.LibraryID, item.FilePath) if err != nil { return nil, fmt.Errorf("cannot resolve library folder: %w", err) diff --git a/templates/book_detail.templ b/templates/book_detail.templ index 46628e6b..6743c0ff 100644 --- a/templates/book_detail.templ +++ b/templates/book_detail.templ @@ -176,7 +176,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string, tomb { book.NotesCount + book.HighlightsCount } } - if user.Role == "admin" { + if user.Role == "admin" && book.LibraryTypeName != "podcasts" {