feat(reader): synchronize Storyteller reading progress

Keep audiobook, EPUB, Kobo, and KOReader positions coherent while serving each client the safest suitable book variant.
This commit is contained in:
neon
2026-09-19 21:27:53 +00:00
parent 4dabacb2b3
commit d3f3382bfa
38 changed files with 1722 additions and 174 deletions
@@ -901,7 +901,7 @@ describe('UploadService', () => {
function mockElection(
lockedBook: { primaryFileId: number | null; status: string; formatPriority: string[] },
files: Array<{ id: number; format: string | null; sizeBytes: number | null }>,
files: Array<{ id: number; format: string | null; sizeBytes: number | null; mediaOverlayAvailable?: boolean }>,
) {
tx.select
.mockReset()
@@ -1025,6 +1025,19 @@ describe('UploadService', () => {
expect(result.role).toBe('content');
});
it('promotes an uploaded read-aloud EPUB over an automatically selected plain EPUB', async () => {
db.select.mockReturnValueOnce(selectJoinChain([makeBookRow()])).mockReturnValueOnce(noHashConflict());
mockElection({ primaryFileId: 99, status: 'present', formatPriority: ['epub'] }, [
{ id: 55, format: 'epub', sizeBytes: 456, mediaOverlayAvailable: true },
{ id: 99, format: 'epub', sizeBytes: 1000, mediaOverlayAvailable: false },
]);
const result = await service.addFileToBook(10, 'book.epub', {} as any, user);
expect(updateSet).toHaveBeenCalledWith(expect.objectContaining({ primaryFileId: 55 }));
expect(result.role).toBe('primary');
});
it('repairs a stale primary reference using the highest-priority eligible file', async () => {
mockUploadedFormat('m4b');
db.select.mockReturnValueOnce(selectJoinChain([makeBookRow({ primaryFileId: 777 })])).mockReturnValueOnce(noHashConflict());