diff --git a/client/src/features/reader/audiobook/__tests__/AudiobookReaderView.spec.ts b/client/src/features/reader/audiobook/__tests__/AudiobookReaderView.spec.ts index 5a6fc8010..57dc9211b 100644 --- a/client/src/features/reader/audiobook/__tests__/AudiobookReaderView.spec.ts +++ b/client/src/features/reader/audiobook/__tests__/AudiobookReaderView.spec.ts @@ -196,7 +196,7 @@ function audiobookManifest(): AudiobookManifest { schemaVersion: 2, revision: 'manifest-rev-1', book: { id: BOOK_ID, title: 'The Long Orbit', authors: ['Ada Vance'], narrators: ['Sam Reed'], hasCover: true }, - assets: [{ assetId: 'asset-1', sequence: 0, format: 'm4b', durationMs: 3_600_000, sizeBytes: 4096, etag: 'etag-1' }], + assets: [{ assetId: 'asset-1', fileId: FILE_ID, sequence: 0, format: 'm4b', durationMs: 3_600_000, sizeBytes: 4096, etag: 'etag-1' }], chapters: [{ id: 'chapter-1', title: 'Launch', assetId: 'asset-1', sequence: 0, startMs: 0, endMs: 3_600_000, assetOffsetMs: 0 }], totalDurationMs: 3_600_000, } diff --git a/packages/types/src/audiobook.ts b/packages/types/src/audiobook.ts index bfce4714f..6e3a562ea 100644 --- a/packages/types/src/audiobook.ts +++ b/packages/types/src/audiobook.ts @@ -8,6 +8,8 @@ export const AUDIOBOOK_MANIFEST_VERSION = 2 as const; export interface AudiobookManifestAsset { assetId: string; + /** The book file id, matching `BookDetail.files[].id`, so clients can pair an asset with its file. */ + fileId: number; sequence: number; format: string; durationMs: number | null; diff --git a/server/src/modules/audiobook/audiobook.service.test.ts b/server/src/modules/audiobook/audiobook.service.test.ts index a8ec7fec1..70272b8ca 100644 --- a/server/src/modules/audiobook/audiobook.service.test.ts +++ b/server/src/modules/audiobook/audiobook.service.test.ts @@ -102,8 +102,8 @@ describe('AudiobookService', () => { expect(manifest.schemaVersion).toBe(AUDIOBOOK_MANIFEST_VERSION); expect(manifest.revision).toMatch(/^[0-9a-f]{64}$/); expect(manifest.assets).toEqual([ - expect.objectContaining({ assetId: `aud_${FIRST_PUBLIC_ID}`, sequence: 0, durationMs: 10_125 }), - expect.objectContaining({ assetId: `aud_${SECOND_PUBLIC_ID}`, sequence: 1, durationMs: 20_000 }), + expect.objectContaining({ assetId: `aud_${FIRST_PUBLIC_ID}`, fileId: 11, sequence: 0, durationMs: 10_125 }), + expect.objectContaining({ assetId: `aud_${SECOND_PUBLIC_ID}`, fileId: 12, sequence: 1, durationMs: 20_000 }), ]); expect(manifest.chapters).toEqual([ expect.objectContaining({ sequence: 0, startMs: 0, endMs: 10_125, assetId: `aud_${FIRST_PUBLIC_ID}`, assetOffsetMs: 0 }), @@ -111,6 +111,18 @@ describe('AudiobookService', () => { ]); }); + it('keeps the manifest revision independent of the file ids it exposes', async () => { + const { service, repo } = makeFixture(); + const before = await service.getManifest(30, makeUser()); + const files = await repo.findAudioFiles(); + repo.findAudioFiles.mockResolvedValue(files.map((file) => ({ ...file, id: file.id + 900 }))); + + const after = await service.getManifest(30, makeUser()); + + expect(after.assets.map((asset) => asset.fileId)).toEqual([911, 912]); + expect(after.revision).toBe(before.revision); + }); + it('derives percentage and creates revision one from a matching manifest write', async () => { const { service, repo, bookService } = makeFixture(); const manifest = await service.getManifest(30, makeUser()); diff --git a/server/src/modules/audiobook/audiobook.service.ts b/server/src/modules/audiobook/audiobook.service.ts index 5eb3b67e8..5547f7693 100644 --- a/server/src/modules/audiobook/audiobook.service.ts +++ b/server/src/modules/audiobook/audiobook.service.ts @@ -212,6 +212,7 @@ export class AudiobookService { .digest('hex'); const assets = files.map((file, sequence) => ({ assetId: this.assetId(file.publicId), + fileId: file.id, sequence, format: file.format!, durationMs: file.durationSeconds === null ? null : Math.round(file.durationSeconds * 1000),