fix(stats): record web audiobook sessions as listening

The web audiobook player shared the reading session composable, which never sent a
sessionType, so the server stored every listening session as a read session and counted
audiobook time as reading in activity stats. Let callers pass an optional sessionType and
have the audiobook view send listen. Other readers keep omitting the field, so their
payloads are unchanged. Existing rows are not backfilled.
This commit is contained in:
neon
2026-09-28 22:16:52 +00:00
parent 55110d5b44
commit d8f68a0d38
3 changed files with 41 additions and 1 deletions
@@ -161,7 +161,7 @@ const audioBookmarks = useAudioBookmarks(props.bookId)
// ── Reading session ───────────────────────────────────────────────────────────
const session = useReadingSession(props.fileId, () => ({ percentage: progressPct.value }), { trackingEnabled })
const session = useReadingSession(props.fileId, () => ({ percentage: progressPct.value }), { trackingEnabled, sessionType: 'listen' })
// ── Ticker (updates position every 500ms while playing) ──────────────────────
@@ -108,3 +108,39 @@ describe('useReadingSession - elapsedMinutes', () => {
expect(apiMock).not.toHaveBeenCalled()
})
})
describe('useReadingSession - sessionType', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.clearAllMocks()
apiMock.mockResolvedValue({ ok: true })
})
afterEach(() => {
vi.useRealTimers()
vi.restoreAllMocks()
})
async function savedBody(options?: Parameters<typeof useReadingSession>[2]): Promise<Record<string, unknown>> {
const { onActivity, endSession } = useReadingSession(7, () => ({ percentage: 10 }), options)
onActivity()
await vi.advanceTimersByTimeAsync(20 * 1000)
endSession()
expect(apiMock).toHaveBeenCalledTimes(1)
const [url, init] = apiMock.mock.calls[0] as [string, { body: string }]
expect(url).toBe('/api/v1/books/files/7/sessions')
return JSON.parse(init.body) as Record<string, unknown>
}
it('omits sessionType by default so the server records a read session', async () => {
const body = await savedBody()
expect(body).not.toHaveProperty('sessionType')
expect(body).not.toHaveProperty('source')
})
it('sends the configured sessionType', async () => {
const body = await savedBody({ sessionType: 'listen' })
expect(body.sessionType).toBe('listen')
expect(body).not.toHaveProperty('source')
})
})
@@ -11,8 +11,11 @@ const IDLE_TIMEOUT_MS = 5 * 60 * 1000
const MIN_SESSION_MS = 10 * 1000
const ELAPSED_UPDATE_INTERVAL_MS = 30 * 1000
export type ReadingSessionType = 'read' | 'tts' | 'listen'
export interface ReadingSessionOptions {
trackingEnabled?: MaybeRef<boolean>
sessionType?: ReadingSessionType
}
function generateSessionId(): string {
@@ -150,6 +153,7 @@ export function useReadingSession(bookFileId: number, getProgress: () => Progres
durationSeconds,
progressDelta,
endProgress,
...(options.sessionType ? { sessionType: options.sessionType } : {}),
})
const url = `/api/v1/books/files/${bookFileId}/sessions`