feat(types): expose the book file id on audiobook manifest assets

Clients that download an audiobook get its files from the book detail and its playback order from
the manifest, and had to pair the two by position. The two lists are ordered differently, so an
index pairing can attach the wrong file to an asset. Add the book_files id to each manifest asset
so the pairing can be made by id.

The field is additive and the server always sends it. The manifest revision is hashed from file
and chapter data, not from the response, so existing revisions do not change and clients holding
one do not see a 412 from this deploy.

The iOS app consumes this field to pair downloaded files with manifest assets; the web client
does not read it yet, so a grep of the client alone will show it as unused.
This commit is contained in:
neon
2026-09-28 22:38:37 +00:00
parent 236c1785d0
commit a35e222cb2
4 changed files with 18 additions and 3 deletions
@@ -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());
@@ -212,6 +212,7 @@ export class AudiobookService {
.digest('hex');
const assets = files.map<AudiobookManifestAsset>((file, sequence) => ({
assetId: this.assetId(file.publicId),
fileId: file.id,
sequence,
format: file.format!,
durationMs: file.durationSeconds === null ? null : Math.round(file.durationSeconds * 1000),