DeadAir in the page: a switch per show, one beside the speed, and the time saved (#172)

A DeadAir switch in each show's settings, and the same switch beside
the speed for the show that is playing, drawn as a waveform with its gap
closed up, outlined when off and solid when on, with aria-pressed. A
file the server gives a copy for plays from /media/{id}/deadair. An
episode's line says how much shorter it is, and Settings says how much
DeadAir has saved you.

Turning it on or off mid-episode saves the place with a request it waits
for, not a beacon, then asks for the entry again from the new
GET /api/entries/{feed}/{guid}, whose position is in the clock of the
version it now plays, and carries on there. A copy not made yet plays
from the next time. Turning a show on wakes the copy maker, so the copy
is there in seconds rather than at its next pass.

The browser test plays a real MP3 with long silences, quiet.mp3, from a
show it adds and removes itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 18:18:41 +00:00
parent 16b0686aab
commit 827cc7489f
13 changed files with 194 additions and 10 deletions

View File

@@ -1750,3 +1750,45 @@ test('a feed\'s settings say what downloads, take words as chips, and keep what
await expect(page.locator('#skw .chip, #sblock .chip')).toHaveCount(0);
await page.keyboard.press('Escape');
});
test('DeadAir is off until turned on for a show, then plays its shortened copy and switches back', async ({ page }) => {
// #171/#172. A show of its own, with real audio, added here and left at the end.
const { id } = await (await page.request.post('/api/feeds', { data: { url: 'http://127.0.0.1:8792/quiet.xml' } })).json();
const entry = async () => {
const r = await page.request.get(`/api/feeds/${encodeURIComponent(id)}/entries`);
return (await r.json()).entries?.[0];
};
await expect.poll(async () => (await entry())?.guid, { timeout: 15_000 }).toBe('quiet-1');
await page.request.post(`/api/feeds/${encodeURIComponent(id)}/download-latest`, { data: { count: 1 } });
await expect.poll(async () => !!(await entry())?.enclosures[0]?.path, { timeout: 15_000 }).toBe(true);
// Off for a show until someone turns it on: no copy is made.
expect((await entry()).enclosures[0].deadair).toBeUndefined();
await page.reload();
// On, in the show's settings.
await page.locator('#feedlist .feed', { hasText: 'Quiet Show' }).first().click();
await page.locator('#content .acts [data-a="settings"]').click();
const sw = page.locator('#sdead');
await expect(sw).not.toBeChecked();
await sw.locator('xpath=..').click();
await page.locator('#ssave').click();
// Made at once, not at the next pass: about 1.8 s off each of four 2 s silences.
await expect.poll(async () => (await entry()).enclosures[0].deadair ?? 0, { timeout: 20_000 }).toBeGreaterThan(5);
await page.reload();
await page.locator('#feedlist .feed', { hasText: 'Quiet Show' }).first().click();
const row = page.locator('.ep', { hasText: 'Pauses Between' });
await expect(row).toContainText('s shorter');
// Played from the copy, with the switch beside the speed pressed.
await row.locator('[data-a=play]').click();
await expect.poll(() => page.evaluate(() => document.querySelector('#audio').getAttribute('src'))).toMatch(/\/deadair$/);
await expect(page.locator('#pdead')).toHaveAttribute('aria-pressed', 'true');
// Off from the player: the original, from the same place.
await page.locator('#pdead').click();
await expect(page.locator('#pdead')).toHaveAttribute('aria-pressed', 'false');
await expect.poll(() => page.evaluate(() => document.querySelector('#audio').getAttribute('src'))).toMatch(/\/media\/\d+$/);
expect((await entry()).enclosures[0].deadair).toBeUndefined();
await page.locator('#pclose').click();
await page.request.delete(`/api/feeds/${encodeURIComponent(id)}`);
});

BIN
tests/ui/fixtures/quiet.mp3 Normal file

Binary file not shown.

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd">
<channel><title>Quiet Show</title><link>http://127.0.0.1:8792/</link><description>Real audio with long silences, for DeadAir.</description>
<item><title>Pauses Between</title><guid>quiet-1</guid>
<pubDate>Mon, 15 Sep 2026 10:00:00 +0000</pubDate>
<description>A second of tone, then two of silence, five times.</description>
<enclosure url="http://127.0.0.1:8792/quiet.mp3" length="60518" type="audio/mpeg"/></item>
</channel></rss>