navidrome/server/nativeapi/podcasts.go
Jiho Andrew Lee 775747264b feat(podcast): add podcast feature with UX improvements - #5420
Backend
- Add podcast data model (PodcastChannel, PodcastEpisode) with migrations
- Implement Subsonic API endpoints: getPodcasts, getNewestPodcasts,
  createPodcastChannel, refreshPodcasts, deletePodcastChannel,
  deletePodcastEpisode, downloadPodcastEpisode, getPodcastEpisode
- Add native REST API endpoints: GET/DELETE /api/podcast,
  GET /api/podcast/preview (feed info without creating channel)
- Inject events.Broker into podcast service for SSE support
- Emit PodcastEpisodeProgress SSE events during download (every 512 KB)
  and on completion/error with status field
- Use HTTP Content-Length as fallback when RSS feed omits enclosure size
- Write ID3 tags (title, album, genre=Podcast) to downloaded files via
  ffmpeg so the library scanner reads correct metadata
- Set MediaFile fields (Title, Album, AlbumID=channelId, AlbumArtist,
  Genre) on episode registration
- Add duplicate URL check in AddChannel
- Add ExistsByURL to PodcastChannelRepository

Frontend - Podcast list
- Add grid/list view toggle (Redux podcastViewReducer) matching album list
- New PodcastGridView component with responsive column count (2-6 cols)
- Cover image 100px in table view
- Remove Feed URL column; add inline copy-to-clipboard button

Frontend - Podcast show (episode list)
- Real-time download progress (%) in Status column via SSE, no polling
  - Spinner only before first SSE event; N% once data arrives
  - Size column removed; Downloading badge replaced with progress
- Completed episodes play on row click; separate play button removed
- Play / Shuffle / Play Next / Add to Queue buttons above episode list
  (only shown when completed episodes exist)
- On download completion, reload episodes to obtain streamId for
  immediate playback without page refresh
- Clicking album name in AudioTitle navigates to podcast channel page

Frontend - Podcast create
- Full-width URL input with Fetch Feed Info button and Enter key support
- Preview card (cover image, title, episode count, description) before
  committing channel creation
- Add Channel button appears only after preview; shows already-registered
  message if channel URL exists

Frontend - Playlist
- Album link navigates to podcast channel page for podcast tracks
  (identified by genre=Podcast)
- Artist column shows '-' for podcast tracks with empty artist field

Closes #5420

Signed-off-by: ji-ho lee <search5@gmail.com>
2026-09-19 21:07:04 +09:00

45 lines
1.2 KiB
Go

package nativeapi
import (
"context"
"encoding/json"
"net/http"
"github.com/deluan/rest"
"github.com/go-chi/chi/v5"
"github.com/navidrome/navidrome/core/podcasts"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/server"
)
func (api *Router) addPodcastRoute(r chi.Router) {
constructor := func(ctx context.Context) rest.Repository {
return api.ds.Resource(ctx, model.PodcastChannel{})
}
r.Route("/podcast", func(r chi.Router) {
r.Get("/", rest.GetAll(constructor))
r.Get("/preview", api.podcastPreview)
r.Route("/{id}", func(r chi.Router) {
r.Use(server.URLParamsMiddleware)
r.Get("/", rest.Get(constructor))
r.Delete("/", rest.Delete(constructor))
})
})
}
func (api *Router) podcastPreview(w http.ResponseWriter, r *http.Request) {
feedURL := r.URL.Query().Get("url")
if feedURL == "" {
http.Error(w, "url is required", http.StatusBadRequest)
return
}
feed, err := podcasts.ParseFeedPreview(feedURL)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
exists, _ := api.ds.PodcastChannel(r.Context()).ExistsByURL(feedURL)
feed.AlreadyExists = exists
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(feed)
}