mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-10-27 23:35:17 -07:00
fix(plugins): delay card-mined until field grouping and YouTube media
- Hold a sentence card's ready report until field grouping finishes - Report cards waiting on queued YouTube media once it is applied or the download fails - Proxy reports added notes ready right away when auto-update is off - Polling keeps enriching the rest of a batch after one note fails - Update plugin and architecture docs
This commit is contained in:
10 files changed
+210
-24
No files matched your search
@@ -54,6 +54,40 @@ test('proxy enqueues addNote result for enrichment and reports it ready afterwar
|
||||
assert.deepEqual(events, ['process 42', 'ready 42']);
|
||||
});
|
||||
|
||||
test('proxy reports an added note ready right away when auto-update is off', () => {
|
||||
const processed: number[] = [];
|
||||
const recordedCards: number[] = [];
|
||||
const ready: number[][] = [];
|
||||
const proxy = new AnkiConnectProxyServer({
|
||||
shouldAutoUpdateNewCards: () => false,
|
||||
processNewCard: async (noteId) => {
|
||||
processed.push(noteId);
|
||||
},
|
||||
recordCardsAdded: (count) => {
|
||||
recordedCards.push(count);
|
||||
},
|
||||
onCardsReady: (noteIds) => {
|
||||
ready.push(noteIds);
|
||||
},
|
||||
logInfo: () => undefined,
|
||||
logWarn: () => undefined,
|
||||
logError: () => undefined,
|
||||
});
|
||||
|
||||
(
|
||||
proxy as unknown as {
|
||||
maybeEnqueueFromRequest: (request: Record<string, unknown>, responseBody: Buffer) => void;
|
||||
}
|
||||
).maybeEnqueueFromRequest(
|
||||
{ action: 'addNote' },
|
||||
Buffer.from(JSON.stringify({ result: 42, error: null }), 'utf8'),
|
||||
);
|
||||
|
||||
assert.deepEqual(ready, [[42]]);
|
||||
assert.deepEqual(processed, []);
|
||||
assert.deepEqual(recordedCards, []);
|
||||
});
|
||||
|
||||
test('proxy enqueues addNote bare numeric response for enrichment', async () => {
|
||||
const processed: number[] = [];
|
||||
const proxy = new AnkiConnectProxyServer({
|
||||
|
||||
@@ -218,15 +218,17 @@ export class AnkiConnectProxyServer {
|
||||
requestJson: Record<string, unknown> | null,
|
||||
responseBody: Buffer,
|
||||
): void {
|
||||
if (!requestJson || !this.deps.shouldAutoUpdateNewCards()) {
|
||||
if (!requestJson) {
|
||||
return;
|
||||
}
|
||||
const autoUpdate = this.deps.shouldAutoUpdateNewCards();
|
||||
|
||||
const action =
|
||||
typeof requestJson.action === 'string'
|
||||
? requestJson.action
|
||||
: String(requestJson.action ?? '');
|
||||
if (action === 'updateNoteFields') {
|
||||
if (!autoUpdate) return;
|
||||
const params = requestJson.params;
|
||||
if (
|
||||
!params ||
|
||||
@@ -279,7 +281,12 @@ export class AnkiConnectProxyServer {
|
||||
params.subminerEnrich === false
|
||||
) {
|
||||
// Stats owns the saved sentence and media; the live mpv context is unrelated.
|
||||
if (noteIds.length > 0) this.deps.recordCardsAdded?.(noteIds.length, noteIds);
|
||||
if (autoUpdate && noteIds.length > 0) this.deps.recordCardsAdded?.(noteIds.length, noteIds);
|
||||
return;
|
||||
}
|
||||
if (!autoUpdate) {
|
||||
// Nothing to enrich, so the added notes are already in their final state.
|
||||
if (noteIds.length > 0) this.deps.onCardsReady?.(noteIds);
|
||||
return;
|
||||
}
|
||||
this.maybeTrackDuplicateNoteIds(requestJson, action, responseResult);
|
||||
|
||||
@@ -152,7 +152,7 @@ interface CardCreationDeps {
|
||||
trackLastAddedDuplicateNoteIds?: (noteId: number, duplicateNoteIds: number[]) => void;
|
||||
findDuplicateNoteIds?: (expression: string, noteInfo: CardCreationNoteInfo) => Promise<number[]>;
|
||||
recordCardsMinedCallback?: (count: number, noteIds?: number[]) => void;
|
||||
/** Called once a created card has all its fields and media written. */
|
||||
/** Called once card creation is done writing a card; queued YouTube media may still be pending. */
|
||||
onCardsReady?: (noteIds: number[]) => void;
|
||||
reviewMediaTiming?: (
|
||||
request: Omit<MediaTimingReviewRequest, 'audioPadding' | 'maxMediaDuration'>,
|
||||
|
||||
@@ -56,6 +56,8 @@ export interface PendingYoutubeMediaQueueDeps {
|
||||
) => string;
|
||||
showStatusNotification: (message: string) => void;
|
||||
showNotification: (noteId: number, label: string | number, errorSuffix?: string) => Promise<void>;
|
||||
/** Called once a queued note's media was applied or dropped, so the card is in its final state. */
|
||||
onUpdateSettled?: (noteId: number) => void;
|
||||
logInfo: (message: string, ...args: unknown[]) => void;
|
||||
logWarn: (message: string, ...args: unknown[]) => void;
|
||||
logError: (message: string, ...args: unknown[]) => void;
|
||||
@@ -79,9 +81,18 @@ function shouldGenerateImage(config: AnkiConnectConfig): boolean {
|
||||
|
||||
export class PendingYoutubeMediaQueue {
|
||||
private updates: PendingYoutubeMediaUpdate[] = [];
|
||||
// Jobs taken off the queue whose media is still being applied or dropped.
|
||||
private settlingNoteIds = new Set<number>();
|
||||
|
||||
constructor(private readonly deps: PendingYoutubeMediaQueueDeps) {}
|
||||
|
||||
/** True while the note still waits on queued media. */
|
||||
hasPendingNote(noteId: number): boolean {
|
||||
return (
|
||||
this.settlingNoteIds.has(noteId) || this.updates.some((update) => update.noteId === noteId)
|
||||
);
|
||||
}
|
||||
|
||||
enqueue(job: PendingYoutubeMediaUpdate): void {
|
||||
if (!job.generateAudio && !job.generateImage) {
|
||||
return;
|
||||
@@ -195,6 +206,8 @@ export class PendingYoutubeMediaQueue {
|
||||
'Failed to apply queued YouTube media update:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
this.settle(job);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,16 +247,24 @@ export class PendingYoutubeMediaQueue {
|
||||
'Failed to show queued YouTube media failure notification:',
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
this.settle(job);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private settle(job: PendingYoutubeMediaUpdate): void {
|
||||
this.settlingNoteIds.delete(job.noteId);
|
||||
this.deps.onUpdateSettled?.(job.noteId);
|
||||
}
|
||||
|
||||
private takeMatchingUpdates(sourceUrl: string): PendingYoutubeMediaUpdate[] {
|
||||
const matched: PendingYoutubeMediaUpdate[] = [];
|
||||
const remaining: PendingYoutubeMediaUpdate[] = [];
|
||||
for (const job of this.updates) {
|
||||
if (youtubeMediaUrlsMatch(job.sourceUrl, sourceUrl)) {
|
||||
matched.push(job);
|
||||
this.settlingNoteIds.add(job.noteId);
|
||||
} else {
|
||||
remaining.push(job);
|
||||
}
|
||||
|
||||
@@ -49,3 +49,47 @@ test('polling runner records new cards and reports each ready after enrichment',
|
||||
Date.now = originalDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
test('polling runner keeps going after one note fails to enrich', async () => {
|
||||
const originalDateNow = Date.now;
|
||||
const events: string[] = [];
|
||||
const warnings: unknown[][] = [];
|
||||
let tracked = new Set<number>();
|
||||
const responses = [[10], [10, 12, 13]];
|
||||
try {
|
||||
Date.now = () => 120_000;
|
||||
const runner = new PollingRunner({
|
||||
getDeck: () => undefined,
|
||||
getPollingRate: () => 250,
|
||||
findNotes: async () => responses.shift() ?? [],
|
||||
shouldAutoUpdateNewCards: () => true,
|
||||
processNewCard: async (noteId) => {
|
||||
events.push(`process ${noteId}`);
|
||||
if (noteId === 12) throw new Error('overlay callback failed');
|
||||
},
|
||||
onCardsReady: (noteIds) => {
|
||||
events.push(`ready ${noteIds.join(',')}`);
|
||||
},
|
||||
isUpdateInProgress: () => false,
|
||||
setUpdateInProgress: () => undefined,
|
||||
getTrackedNoteIds: () => tracked,
|
||||
setTrackedNoteIds: (noteIds) => {
|
||||
tracked = noteIds;
|
||||
},
|
||||
showStatusNotification: () => undefined,
|
||||
logDebug: () => undefined,
|
||||
logInfo: () => undefined,
|
||||
logWarn: (...args) => {
|
||||
warnings.push(args);
|
||||
},
|
||||
});
|
||||
|
||||
await runner.pollOnce();
|
||||
await runner.pollOnce();
|
||||
|
||||
assert.deepEqual(events, ['process 12', 'ready 12', 'process 13', 'ready 13']);
|
||||
assert.deepEqual(warnings, [['Failed to auto-enrich note 12:', 'overlay callback failed']]);
|
||||
} finally {
|
||||
Date.now = originalDateNow;
|
||||
}
|
||||
});
|
||||
@@ -89,6 +89,9 @@ export class PollingRunner {
|
||||
for (const noteId of newNoteIds) {
|
||||
try {
|
||||
await this.deps.processNewCard(noteId);
|
||||
} catch (error) {
|
||||
// One failed note must not leave the rest of the batch unprocessed.
|
||||
this.deps.logWarn(`Failed to auto-enrich note ${noteId}:`, (error as Error).message);
|
||||
} finally {
|
||||
this.deps.onCardsReady?.([noteId]);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user