commit d188fe4a5acd8051e52cdfc324534f19e7016632 truex <truex@equwal.com> 2026-09-26 23:40:12 -0700 Offer a SubRead result for a closed book when that book opens A SubRead result for a book that was no longer open went straight into the subtitles of that book: makeSubtitles when the book changed while it read the result file, and checkPendingSubtitles when the book changed while it waited for the plugin. The result then skipped the confirm, and the checks of the language and the match rate, which exist because SubRead 0.9.1 gave "km" and a match rate of 1 for audio with no speech. The result now waits in the meta data of its book as a pending result (BookMeta.subread, a KeptResult with the .srt, the cues, the language and the match rate), not as its subtitles. When the book opens, applyMeta offers it with offerSubreadSubtitles, the same confirm and checks as for the open book, and removes it. makeSubtitles also reads the result file when another book opened during the job, so that the result keeps what SubRead told about it for the checks. Before, that path left only the .srt in the result file, and a SubRead older than 0.10 lost the result.
README.md | 3 +++ src/main.ts | 36 ++++++++++++++++++++++++++---------- src/messages.ts | 5 +++++ src/token-cache.ts | 18 ++++++++++++++++++ test/messages.test.ts | 7 +++++++ test/token-cache.test.ts | 18 ++++++++++++++++++ 6 files changed, 77 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md index f3d5b20..152e556 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,9 @@ make before then stays. - Before the subtitles of SubRead load, the app asks when they replace a file that you loaded, when SubRead found another language than the one of the book, and when it found less than 80% of the lines. +- A SubRead result for a book that is not open waits in the meta data of + that book. When you open the book, the app offers it with the same + questions. ### The status strip diff --git a/src/main.ts b/src/main.ts index eac2625..f7bd180 100644 --- a/src/main.ts +++ b/src/main.ts @@ -41,6 +41,7 @@ import { clockText, EMPTY_FILE_TEXT, errorMessage, + keptResultText, LAST_BOOK_GONE_TEXT, matchedText, noCuesText, @@ -87,6 +88,7 @@ import { putPage, type BookMeta, type BookSubtitles, + type KeptResult, type PageEntry, } from './token-cache'; import { createPageView, type Marks } from './view'; @@ -469,8 +471,14 @@ function applyMeta(meta: BookMeta, attempt: number, subtitlesSeq: number): void break; } } - // After the saved subtitles: a SubRead result for a book with a file of - // the user asks first. + // After the saved subtitles, so that a SubRead result asks first when it + // replaces a file of the user. A result that came while another book was + // open gets the same offer as the result for the open book. + const kept = meta.subread; + if (kept) { + void putMeta(key, { subread: undefined }); + if (offerSubreadSubtitles(kept.srt, kept)) ui.makeStatus.textContent = subreadResultText(kept); + } void checkPendingSubtitles(); } @@ -928,11 +936,6 @@ async function makeSubtitles(audio: { uri: string; name: string }, book: string) state.making = null; ui.makeSubs.disabled = state.book === null; } - // The result file waits for its book: pendingSubtitles loads it when the book opens. - if (state.book !== book) { - ui.makeStatus.textContent = 'SubRead finished, but another book is open now.'; - return; - } if (result.error === 'not_installed') { fill(ui.makeStatus, SUBREAD_NOT_INSTALLED); return; @@ -945,14 +948,27 @@ async function makeSubtitles(audio: { uri: string; name: string }, book: string) ui.makeStatus.textContent = subreadErrorText(result.error ?? 'no file'); return; } + // Another book opened during the job: the result waits for its book, + // with what SubRead told about it, for the checks of the offer. if (state.book !== book) { - void putMeta(book, { subtitles: subreadSubtitles(book, srt) }); + const { cues, matchRate, language } = result; + keepResult(book, { srt, cues, matchRate, language }); return; } ui.makeStatus.textContent = subreadResultText(result); offerSubreadSubtitles(srt, result); } +/** + * Keeps a SubRead result for the book `book`, which is not open, in its + * meta data. It is not saved as the subtitles of the book: applyMeta + * offers it when the book opens, with the confirm and the checks. + */ +function keepResult(book: string, kept: KeptResult): void { + void putMeta(book, { subread: kept }); + ui.makeStatus.textContent = keptResultText(bookName(book)); +} + type PendingSrt = { srt?: string }; /** Subtitles that SubRead made for the book `book`. */ @@ -1004,9 +1020,9 @@ async function checkPendingSubtitles(): Promise<void> { } const srt = pending.srt; if (srt === undefined) return; - // The call removed the file, so the .srt goes to the meta data of its book. + // The call removed the file, so the .srt waits in the meta data of its book. if (state.book !== book) { - void putMeta(book, { subtitles: subreadSubtitles(book, srt) }); + keepResult(book, { srt }); return; } if (offerSubreadSubtitles(srt, {})) { diff --git a/src/messages.ts b/src/messages.ts index 9a8029e..8440df3 100644 --- a/src/messages.ts +++ b/src/messages.ts @@ -216,6 +216,11 @@ export function subreadResultText(r: { ); } +/** SubRead made the subtitles of the book `name`, which is not open. They wait for that book. */ +export function keptResultText(name: string): string { + return `SubRead made the subtitles of ${name}. The reader offers them when you open that book.`; +} + /** Why SubRead made no subtitles. `cannot_start` means that the read grant of the audio is gone. */ export function subreadErrorText(error: string): string { if (error.startsWith('cannot_start')) { diff --git a/src/token-cache.ts b/src/token-cache.ts index 659201b..9c45162 100644 --- a/src/token-cache.ts +++ b/src/token-cache.ts @@ -44,6 +44,22 @@ export interface BookSubtitles { source: 'file' | 'subread'; } +/** + * A SubRead result that came while another book was open. It waits in the + * meta data of its book, and the reader offers it, with the confirm and the + * checks, when the book opens (see subreadConcerns). + */ +export interface KeptResult { + /** The text of the .srt. */ + srt: string; + /** The number of cues, when SubRead told it. */ + cues?: number; + /** 0 to 1: the share of the lines that SubRead found in the book, when it told it. */ + matchRate?: number; + /** The language that SubRead found, when it told it. */ + language?: string | null; +} + /** What the reader keeps for a book, other than its pages. */ export interface BookMeta { /** The page that was open. 0 is the first page. */ @@ -51,6 +67,8 @@ export interface BookMeta { subtitles?: BookSubtitles; /** The "Force OCR" setting for the book. */ forceOcr?: boolean; + /** A SubRead result that waits for the next open of the book. */ + subread?: KeptResult; } /** The record of the last book. */ diff --git a/test/messages.test.ts b/test/messages.test.ts index 116ad76..44f0762 100644 --- a/test/messages.test.ts +++ b/test/messages.test.ts @@ -22,6 +22,7 @@ import { playerProblem, readingEndMessage, readingText, + keptResultText, shownInPlayerPart, subreadErrorText, subreadQuestion, @@ -289,6 +290,12 @@ describe('SubRead texts', () => { ); }); + it('tells that the result for a book that is not open waits for that book', () => { + expect(keptResultText('neko.pdf')).toBe( + 'SubRead made the subtitles of neko.pdf. The reader offers them when you open that book.', + ); + }); + it('asks the user to choose the audio again when SubRead cannot start', () => { expect(subreadErrorText('cannot_start: Permission Denial: opening provider')).toBe( 'SubRead cannot read the audio file. Choose the audio again.', diff --git a/test/token-cache.test.ts b/test/token-cache.test.ts index 5e32219..7b04a06 100644 --- a/test/token-cache.test.ts +++ b/test/token-cache.test.ts @@ -128,6 +128,24 @@ describe('token cache', () => { expect(await cache.getMeta('inu.pdf|20')).toEqual({}); }); + it('keeps a SubRead result for a book apart from its subtitles', async () => { + // The finding: a SubRead result for a book that was not open went into + // its subtitles, without the confirm and the checks of the language and + // the match rate. It now waits as a result, and the subtitles stay. + const cache = await freshCache(); + const subtitles: BookSubtitles = { name: 'neko.srt', text: srt, source: 'file' }; + const result = { srt: '1\n00:00:00,000 --> 00:00:01,000\nx\n', cues: 23, language: 'km' }; + await cache.putMeta('neko.pdf|10', { subtitles }); + await cache.putMeta('neko.pdf|10', { subread: { ...result, matchRate: 1 } }); + expect(await cache.getMeta('neko.pdf|10')).toEqual({ + subtitles, + subread: { ...result, matchRate: 1 }, + }); + // The offer of the result removes it. + await cache.putMeta('neko.pdf|10', { subread: undefined }); + expect(await cache.getMeta('neko.pdf|10')).toEqual({ subtitles }); + }); + it('keeps the fields of two merges at the same time', async () => { const cache = await freshCache(); await Promise.all([