don't block during scan

This commit is contained in:
Garret Patti
2026-04-20 08:28:43 -04:00
parent 71a026f01e
commit a6d657d87d
5 changed files with 223 additions and 112 deletions

View File

@@ -573,29 +573,37 @@ async function scanComics(library: Library, libraryRoot: string): Promise<void>
scanned_at = excluded.scanned_at
`)
type SeriesRec = Parameters<typeof upsertSeries.run>[0]
type IssueRec = Parameters<typeof upsertIssue.run>[0]
type BatchEntry = { type: 'series'; rec: SeriesRec } | { type: 'issue'; rec: IssueRec }
// Collect all records before touching the DB so we can batch-insert with event-loop yields.
// Note: between clearLibraryItems and the final batch, the library will appear partially
// populated — acceptable for a background scan.
const allRecords: BatchEntry[] = []
let issueCount = 0
db.transaction(() => {
for (const item of items) {
if ('issues' in item) {
const series = item as ScannedComicSeries
const seriesKey = `${library.id}:comic_series:${series.id}`
upsertSeries.run({
for (const item of items) {
if ('issues' in item) {
const series = item as ScannedComicSeries
const seriesKey = `${library.id}:comic_series:${series.id}`
allRecords.push({
type: 'series',
rec: {
library_id: library.id,
item_key: seriesKey,
item_type: 'comic_series',
title: series.title,
metadata: JSON.stringify({
issueCount: series.issueCount,
coverUrl: series.coverUrl,
}),
metadata: JSON.stringify({ issueCount: series.issueCount, coverUrl: series.coverUrl }),
file_path: null,
scanned_at: now,
})
for (const issue of series.issues) {
const issueKey = `${library.id}:comic_issue:${issue.id}`
upsertIssue.run({
},
})
for (const issue of series.issues) {
const issueKey = `${library.id}:comic_issue:${issue.id}`
allRecords.push({
type: 'issue',
rec: {
library_id: library.id,
item_key: issueKey,
item_type: 'comic_issue',
@@ -609,13 +617,16 @@ async function scanComics(library: Library, libraryRoot: string): Promise<void>
}),
file_path: issue.filePath,
scanned_at: now,
})
issueCount++
}
} else {
const issue = item as ComicIssue
const issueKey = `${library.id}:comic_issue:${issue.id}`
upsertIssue.run({
},
})
issueCount++
}
} else {
const issue = item as ComicIssue
const issueKey = `${library.id}:comic_issue:${issue.id}`
allRecords.push({
type: 'issue',
rec: {
library_id: library.id,
item_key: issueKey,
item_type: 'comic_issue',
@@ -629,13 +640,27 @@ async function scanComics(library: Library, libraryRoot: string): Promise<void>
}),
file_path: issue.filePath,
scanned_at: now,
})
issueCount++
}
},
})
issueCount++
}
})()
}
// Prewarm CBZ cover thumbnails
// Insert in batches of 500, yielding the event loop between batches so the app
// remains responsive to HTTP requests during a large scan.
const BATCH_SIZE = 500
for (let i = 0; i < allRecords.length; i += BATCH_SIZE) {
const batch = allRecords.slice(i, i + BATCH_SIZE)
db.transaction(() => {
for (const entry of batch) {
if (entry.type === 'series') upsertSeries.run(entry.rec)
else upsertIssue.run(entry.rec)
}
})()
await new Promise<void>((r) => setImmediate(r))
}
// Prewarm CBZ cover thumbnails — fire-and-forget so they don't block scan completion.
for (const item of items) {
const issuesToWarm: ComicIssue[] = 'issues' in item
? (item as ScannedComicSeries).issues.slice(0, 1)
@@ -643,11 +668,9 @@ async function scanComics(library: Library, libraryRoot: string): Promise<void>
for (const issue of issuesToWarm) {
const absPath = path.join(libraryRoot, issue.filePath)
try {
await getCbzThumbnailPath(absPath, library.id)
} catch (err) {
getCbzThumbnailPath(absPath, library.id).catch((err) => {
console.warn(`[scanner] Could not generate CBZ thumbnail for ${issue.filePath}:`, err instanceof Error ? err.message : err)
}
})
}
}