From 37e33c656ba3c32301e6b565163c26ea2cf71953 Mon Sep 17 00:00:00 2001 From: mrleo1nid <43583385+mrleo1nid@users.noreply.github.com> Date: Thu, 29 Oct 2026 11:21:48 +0300 Subject: [PATCH] fix(uploads): allow signed audio previews and compact destination details --- apps/web/e2e/upload-review.spec.ts | 26 +++++++-- apps/web/src/features/uploads/QueueRows.tsx | 11 +--- docs/uploads.md | 4 +- packages/i18n/locales/en/admin.json | 2 - packages/i18n/locales/ru/admin.json | 2 - services/api/internal/auth/middleware.go | 18 ++++++ .../internal/auth/middleware_stream_test.go | 38 +++++++++++++ services/api/internal/uploads/stream_test.go | 55 +++++++++++++++++++ 8 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 services/api/internal/auth/middleware_stream_test.go create mode 100644 services/api/internal/uploads/stream_test.go diff --git a/apps/web/e2e/upload-review.spec.ts b/apps/web/e2e/upload-review.spec.ts index 0fb7379..a5fb9a7 100644 --- a/apps/web/e2e/upload-review.spec.ts +++ b/apps/web/e2e/upload-review.spec.ts @@ -13,7 +13,7 @@ async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') { const source = structuredClone(mobileFixtures['/api/v1/admin/sources'].items[0]); const file = structuredClone(mobileFixtures['/api/v1/admin/uploads'].items[0]); const files = [ - { ...file, id: 'visible', original_name: 'visible.flac', target_source_name: source.name, target_rel_path: 'Artist/Album/01 - Song.flac', target_storage_path: 'nas:/music/Artist/Album/01 - Song.flac' }, + { ...file, id: 'visible', original_name: 'visible.flac', target_source_name: source.name, target_rel_path: 'Artist/Album/01 - Song.flac', target_storage_path: 'nas:/music/Artist/Album/01 - Song.flac', stream_url: `/api/v1/admin/uploads/files/${file.id}/stream?exp=9999999999&sig=test` }, { ...file, id: 'approved', original_name: 'approved.flac', status: 'approved' }, { ...file, id: 'hidden', original_name: 'hidden.flac', status: 'processing' }, { ...file, id: 'withdrawn', original_name: 'withdrawn.flac', metadata: null, status: 'withdrawn' }, @@ -78,7 +78,7 @@ test('queue shows the storage and path, and recalculates after fixing placement' await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeHidden(); await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click(); await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeVisible(); - await expect(page.getByText('nas:/music/Artist/Album/01 - Song.flac', { exact: true })).toBeVisible(); + await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toHaveAttribute('title', 'nas:/music/Artist/Album/01 - Song.flac'); await expect(page.getByText('Хранилище не выбрано', { exact: true })).toBeVisible(); await page.getByRole('checkbox', { name: 'Выбрать всю пачку', exact: true }).check(); await page.getByRole('button', { name: 'Свернуть пачку', exact: true }).click(); @@ -156,7 +156,7 @@ test('queue shows the current full destination after saving a changed source tem }, path); await navigate('/admin/uploads'); await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click(); - await expect(page.getByText('nas:/music/Artist/Album/Song.flac', { exact: true })).toBeVisible(); + await expect(page.getByText('Artist/Album/Song.flac', { exact: true })).toHaveAttribute('title', 'nas:/music/Artist/Album/Song.flac'); await navigate(sourcePath); await page.getByRole('textbox', { name: 'Шаблон пути', exact: true }).fill('{albumartist}/{album}/{track:02}. {title}.{ext}'); const section = page.getByRole('heading', { name: 'Приём загрузок и шаблон пути' }).locator('..'); @@ -165,6 +165,24 @@ test('queue shows the current full destination after saving a changed source tem await expect(page.locator('html')).toHaveAttribute('data-saved-template', '{albumartist}/{album}/{track:02}. {title}.{ext}'); await navigate('/admin/uploads'); await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click(); - await expect(page.getByText('nas:/music/Artist/Album/01. Song.flac', { exact: true })).toBeVisible(); + await expect(page.getByText('Artist/Album/01. Song.flac', { exact: true })).toHaveAttribute('title', 'nas:/music/Artist/Album/01. Song.flac'); await expect(page.getByText('Artist/Album/Song.flac', { exact: true })).toHaveCount(0); }); + +test('queue preview loads duration before play and plays signed audio', async ({ page }) => { + const dataSize = 8000 * 2 * 2; + const wav = Buffer.alloc(44 + dataSize); + wav.write('RIFF', 0); wav.writeUInt32LE(36 + dataSize, 4); wav.write('WAVEfmt ', 8); + wav.writeUInt32LE(16, 16); wav.writeUInt16LE(1, 20); wav.writeUInt16LE(1, 22); + wav.writeUInt32LE(8000, 24); wav.writeUInt32LE(16000, 28); wav.writeUInt16LE(2, 32); wav.writeUInt16LE(16, 34); + wav.write('data', 36); wav.writeUInt32LE(dataSize, 40); + await page.route('**/api/v1/admin/uploads/files/*/stream?*', (route) => route.fulfill({ contentType: 'audio/wav', body: wav })); + await setup(page, 'queue'); + await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click(); + const audio = page.locator('audio').first(); + await expect(audio).toHaveAttribute('preload', 'metadata'); + await expect.poll(() => audio.evaluate((el: HTMLAudioElement) => el.duration)).toBe(2); + await audio.evaluate((el: HTMLAudioElement) => el.play()); + await expect.poll(() => audio.evaluate((el: HTMLAudioElement) => el.currentTime)).toBeGreaterThan(0); + expect(await audio.evaluate((el: HTMLAudioElement) => el.error)).toBeNull(); +}); diff --git a/apps/web/src/features/uploads/QueueRows.tsx b/apps/web/src/features/uploads/QueueRows.tsx index bc239eb..576ced3 100644 --- a/apps/web/src/features/uploads/QueueRows.tsx +++ b/apps/web/src/features/uploads/QueueRows.tsx @@ -142,7 +142,7 @@ function QueueRow({
{f.stream_url && ( -