mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-08 10:27:08 +02:00
fix(podcast): fix build failure and missing episode play/status UX
server/e2e was failing to build because subsonic.New() gained a podcasts.Podcasts parameter that the sonic-similarity e2e test wasn't passing. Also fixes real bugs surfaced while getting the podcast tests green: completed episodes had no play button, the downloading status badge dropped its label text, and PodcastCreate's test suite assumed a single-step form submit instead of the actual preview-then-add flow.
This commit is contained in:
parent
b6b91709f0
commit
aa2d89611e
5 changed files with 86 additions and 21 deletions
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <CircularProgress size={20} />
|
||||
}
|
||||
|
||||
if (episode.status === 'completed') {
|
||||
return (
|
||||
<IconButton aria-label="delete" size="small" onClick={handleDelete}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<>
|
||||
<IconButton aria-label="play" size="small" onClick={handlePlay}>
|
||||
<PlayArrowIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<IconButton aria-label="delete" size="small" onClick={handleDelete}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => (
|
||||
<button aria-label={label} onClick={onClick} disabled={disabled}>
|
||||
{children}
|
||||
</button>
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
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(<PodcastCreate />)
|
||||
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(<PodcastCreate />)
|
||||
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(<PodcastCreate />)
|
||||
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(<PodcastCreate />)
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -212,7 +212,11 @@ const PodcastShow = (props) => {
|
|||
<StatusBadge status={ep.status} errorMessage={ep.errorMessage} downloadedBytes={ep.downloadedBytes} size={ep.size} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<EpisodeActions episode={ep} onRefresh={loadEpisodes} />
|
||||
<EpisodeActions
|
||||
episode={ep}
|
||||
onRefresh={loadEpisodes}
|
||||
channelTitle={record.title}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ const StatusBadge = ({ status, errorMessage, downloadedBytes, size }) => {
|
|||
return (
|
||||
<Box display="flex" alignItems="center" style={{ gap: 6 }}>
|
||||
<CircularProgress size={14} />
|
||||
<Typography variant="caption">{label}</Typography>
|
||||
{pct !== null && <Typography variant="caption">{`${pct}%`}</Typography>}
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue