Do not offer a player for a file that is not audio or video

The media-type filter stopped new image enclosures being fetched, but ones
already on disk still got a play button and an <audio> element, because
the UI tested for a path rather than for a playable type. Four places did
this, including play() itself, which picked the first downloaded enclosure
whatever it was.

isPlayable() checks audio/* or video/*, falling back to the extension when
a feed declares no type. A downloaded non-media file now shows as its kind
with Save and Delete, so it stays available without posing as an episode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
This commit is contained in:
2026-09-10 17:50:39 +00:00
parent ddb9dbb7e1
commit 6d6b4dd1e4
6 changed files with 85 additions and 6 deletions

View File

@@ -10,7 +10,8 @@ test.beforeEach(async ({ page }) => {
test('the page loads and lists the configured feeds', async ({ page }) => {
// Regression: a ReferenceError in the script left the shell rendered and the sidebar
// empty, with every handler below the error dead. Server-side checks all passed.
await expect(page.locator('.feed')).toHaveCount(2, { timeout: 15_000 });
// Three top-level feeds in the fixture config; the OPML's child is inside a closed folder.
await expect(page.locator('.feed')).toHaveCount(3, { timeout: 15_000 });
await expect(page.getByText('Test Show')).toBeVisible();
const errors = [];
page.on('pageerror', e => errors.push(e.message));
@@ -84,6 +85,24 @@ test('the three panes are there and the item text lands in the bottom one', asyn
await expect(page.locator('#detail audio')).toHaveCount(0);
});
test('a downloaded file that is not audio gets no player', async ({ page }) => {
// Regression: anything with a file got an <audio> element and a play button, so a blog's
// header image rendered as a broken player.
await page.locator('.feed', { hasText: 'Picture Blog' }).click();
const row = page.locator('.ep', { hasText: 'An Article' });
await expect(row).toBeVisible({ timeout: 20_000 });
await expect(row.locator('[data-a="play"]')).toHaveCount(0);
await row.click();
await expect(page.locator('#detail .dt')).toHaveText('An Article');
await expect(page.locator('#detail audio')).toHaveCount(0);
await expect(page.locator('#detail .encbox')).toContainText('image');
await expect(page.locator('#detail .encbox')).toContainText('downloaded');
// Still offered as a file, just not as an episode.
await expect(page.locator('#detail .btn', { hasText: 'Save' })).toBeVisible();
});
test('the filter tabs change what is listed', async ({ page }) => {
await page.getByText('Test Show').click();
await expect(page.locator('.ep').first()).toBeVisible({ timeout: 20_000 });