diff --git a/server/subsonic/e2e/subsonic_sonic_similarity_test.go b/server/subsonic/e2e/subsonic_sonic_similarity_test.go
index c0cb1d359..3c71a994c 100644
--- a/server/subsonic/e2e/subsonic_sonic_similarity_test.go
+++ b/server/subsonic/e2e/subsonic_sonic_similarity_test.go
@@ -10,11 +10,13 @@ import (
"github.com/navidrome/navidrome/core"
"github.com/navidrome/navidrome/core/agents"
"github.com/navidrome/navidrome/core/artwork"
+ "github.com/navidrome/navidrome/core/ffmpeg"
"github.com/navidrome/navidrome/core/lyrics"
"github.com/navidrome/navidrome/core/matcher"
"github.com/navidrome/navidrome/core/metrics"
"github.com/navidrome/navidrome/core/playback"
"github.com/navidrome/navidrome/core/playlists"
+ "github.com/navidrome/navidrome/core/podcasts"
"github.com/navidrome/navidrome/core/scrobbler"
"github.com/navidrome/navidrome/core/sonic"
"github.com/navidrome/navidrome/core/stream"
@@ -51,6 +53,7 @@ func buildSonicRouter(provider sonic.Provider) *subsonic.Router {
metrics.NewNoopInstance(),
lyrics.NewLyrics(ds, nil),
decider,
+ podcasts.NewPodcastService(context.Background(), ds, ffmpeg.New(), events.NoopBroker()),
sonicSvc,
)
}
diff --git a/ui/src/podcast/EpisodeActions.jsx b/ui/src/podcast/EpisodeActions.jsx
index ce55cc0c8..edea9bd34 100644
--- a/ui/src/podcast/EpisodeActions.jsx
+++ b/ui/src/podcast/EpisodeActions.jsx
@@ -1,30 +1,56 @@
import React from 'react'
+import { useDispatch } from 'react-redux'
import { IconButton, CircularProgress } from '@material-ui/core'
import GetAppIcon from '@material-ui/icons/GetApp'
import DeleteIcon from '@material-ui/icons/Delete'
+import PlayArrowIcon from '@material-ui/icons/PlayArrow'
import subsonic from '../subsonic'
+import { setTrack } from '../actions'
-const EpisodeActions = ({ episode, onRefresh }) => {
+const EpisodeActions = ({ episode, onRefresh, channelTitle }) => {
+ const dispatch = useDispatch()
const handleDownload = async () => {
await subsonic.downloadPodcastEpisode(episode.id)
onRefresh?.()
}
- const handleDelete = async () => {
+ const handleDelete = async (e) => {
+ e.stopPropagation()
await subsonic.deletePodcastEpisode(episode.id)
onRefresh?.()
}
+ const handlePlay = (e) => {
+ e.stopPropagation()
+ dispatch(
+ setTrack({
+ id: episode.streamId,
+ title: episode.title,
+ album: channelTitle || episode.channelId,
+ artist: '',
+ duration: episode.duration,
+ suffix: episode.suffix,
+ isPodcast: true,
+ channelId: episode.channelId,
+ }),
+ )
+ }
+
if (episode.status === 'downloading') {
return
}
if (episode.status === 'completed') {
return (
-
-
-
+ <>
+
+
+
+
+
+
+ >
)
}
diff --git a/ui/src/podcast/PodcastCreate.test.jsx b/ui/src/podcast/PodcastCreate.test.jsx
index 2a88379a4..948594439 100644
--- a/ui/src/podcast/PodcastCreate.test.jsx
+++ b/ui/src/podcast/PodcastCreate.test.jsx
@@ -3,7 +3,17 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'
import { describe, it, expect, vi, beforeEach } from 'vitest'
vi.mock('../subsonic', () => ({
- default: { createPodcastChannel: vi.fn().mockResolvedValue({}) },
+ default: {
+ createPodcastChannel: vi.fn().mockResolvedValue({}),
+ previewPodcastFeed: vi.fn().mockResolvedValue({
+ json: {
+ title: 'Example Podcast',
+ description: 'An example feed',
+ episodeCount: 3,
+ alreadyExists: false,
+ },
+ }),
+ },
}))
const mockRedirect = vi.fn()
@@ -19,12 +29,27 @@ vi.mock('react-admin', async (importOriginal) => {
useRedirect: () => mockRedirect,
useRefresh: () => mockRefresh,
Title: () => null,
+ Button: ({ children, onClick, label, disabled }) => (
+
+ ),
}
})
import subsonic from '../subsonic'
import PodcastCreate from './PodcastCreate'
+const fetchPreview = async (url) => {
+ fireEvent.change(screen.getByRole('textbox'), { target: { value: url } })
+ fireEvent.click(
+ screen.getByLabelText('resources.podcast.actions.fetchFeed'),
+ )
+ await waitFor(() => {
+ expect(subsonic.previewPodcastFeed).toHaveBeenCalledWith(url)
+ })
+}
+
describe('PodcastCreate', () => {
beforeEach(() => vi.clearAllMocks())
@@ -33,12 +58,18 @@ describe('PodcastCreate', () => {
expect(screen.getByRole('textbox')).toBeTruthy()
})
- it('calls createPodcastChannel with the entered URL on submit', async () => {
+ it('fetches a preview of the feed for the entered URL', async () => {
render()
- fireEvent.change(screen.getByRole('textbox'), {
- target: { value: 'https://example.com/feed.xml' },
- })
- fireEvent.submit(screen.getByRole('form'))
+ await fetchPreview('https://example.com/feed.xml')
+ expect(await screen.findByText('Example Podcast')).toBeTruthy()
+ })
+
+ it('calls createPodcastChannel with the entered URL when adding the previewed channel', async () => {
+ render()
+ await fetchPreview('https://example.com/feed.xml')
+ fireEvent.click(
+ await screen.findByLabelText('resources.podcast.actions.addChannel'),
+ )
await waitFor(() => {
expect(subsonic.createPodcastChannel).toHaveBeenCalledWith(
'https://example.com/feed.xml',
@@ -46,12 +77,12 @@ describe('PodcastCreate', () => {
})
})
- it('redirects to /podcast after successful submit', async () => {
+ it('redirects to /podcast after successfully adding the channel', async () => {
render()
- fireEvent.change(screen.getByRole('textbox'), {
- target: { value: 'https://example.com/feed.xml' },
- })
- fireEvent.submit(screen.getByRole('form'))
+ await fetchPreview('https://example.com/feed.xml')
+ fireEvent.click(
+ await screen.findByLabelText('resources.podcast.actions.addChannel'),
+ )
await waitFor(() => {
expect(mockRedirect).toHaveBeenCalledWith('/podcast')
})
@@ -59,10 +90,10 @@ describe('PodcastCreate', () => {
it('notifies on success', async () => {
render()
- fireEvent.change(screen.getByRole('textbox'), {
- target: { value: 'https://example.com/feed.xml' },
- })
- fireEvent.submit(screen.getByRole('form'))
+ await fetchPreview('https://example.com/feed.xml')
+ fireEvent.click(
+ await screen.findByLabelText('resources.podcast.actions.addChannel'),
+ )
await waitFor(() => {
expect(mockNotify).toHaveBeenCalledWith(
'resources.podcast.notifications.channelAdded',
diff --git a/ui/src/podcast/PodcastShow.jsx b/ui/src/podcast/PodcastShow.jsx
index 6f98b4d5a..2a462effc 100644
--- a/ui/src/podcast/PodcastShow.jsx
+++ b/ui/src/podcast/PodcastShow.jsx
@@ -212,7 +212,11 @@ const PodcastShow = (props) => {
-
+
))}
diff --git a/ui/src/podcast/StatusBadge.jsx b/ui/src/podcast/StatusBadge.jsx
index fd968e1ee..a78a6f9b5 100644
--- a/ui/src/podcast/StatusBadge.jsx
+++ b/ui/src/podcast/StatusBadge.jsx
@@ -22,6 +22,7 @@ const StatusBadge = ({ status, errorMessage, downloadedBytes, size }) => {
return (
+ {label}
{pct !== null && {`${pct}%`}}
)