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

@@ -126,10 +126,18 @@ export function scanComicsLibrary(
return results.sort((a, b) => naturalCompare(a.title, b.title))
}
// comicsFromDb returns series + standalone issues for the top-level grid.
function escapeLike(s: string): string {
return `%${s.replace(/%/g, '\\%').replace(/_/g, '\\_')}%`
}
// comicsFromDb returns series + standalone issues for the top-level grid, paginated.
// Series issues are retrieved separately via comicIssuesFromDb.
export function comicsFromDb(libraryId: string): (ComicIssue | ComicSeries)[] {
export function comicsFromDb(
libraryId: string,
opts: { page: number; pageSize: number; search?: string }
): { items: (ComicIssue | ComicSeries)[]; total: number } {
const db = getDb()
const offset = (opts.page - 1) * opts.pageSize
type DbRow = {
item_key: string
@@ -140,59 +148,63 @@ export function comicsFromDb(libraryId: string): (ComicIssue | ComicSeries)[] {
file_path: string | null
}
const allRows = db
.prepare(
`SELECT item_key, item_type, parent_key, title, metadata, file_path
FROM media_items
WHERE library_id = ? AND item_type IN ('comic_series','comic_issue')
ORDER BY title`
)
.all(libraryId) as DbRow[]
const baseWhere = `
WHERE library_id = ?
AND (item_type = 'comic_series' OR (item_type = 'comic_issue' AND parent_key IS NULL))
`
const seriesMap = new Map<string, ComicSeries>()
const standaloneIssues: ComicIssue[] = []
const total: number = opts.search
? (db
.prepare(`SELECT COUNT(*) as cnt FROM media_items ${baseWhere} AND title LIKE ? ESCAPE '\\'`)
.get(libraryId, escapeLike(opts.search)) as { cnt: number }).cnt
: (db
.prepare(`SELECT COUNT(*) as cnt FROM media_items ${baseWhere}`)
.get(libraryId) as { cnt: number }).cnt
for (const row of allRows) {
if (row.item_type !== 'comic_series') continue
const rows: DbRow[] = opts.search
? db
.prepare(
`SELECT item_key, item_type, parent_key, title, metadata, file_path
FROM media_items ${baseWhere} AND title LIKE ? ESCAPE '\\'
ORDER BY title LIMIT ? OFFSET ?`
)
.all(libraryId, escapeLike(opts.search), opts.pageSize, offset) as DbRow[]
: db
.prepare(
`SELECT item_key, item_type, parent_key, title, metadata, file_path
FROM media_items ${baseWhere}
ORDER BY title LIMIT ? OFFSET ?`
)
.all(libraryId, opts.pageSize, offset) as DbRow[]
const items: (ComicIssue | ComicSeries)[] = []
for (const row of rows) {
const meta = row.metadata ? JSON.parse(row.metadata) : {}
const idPart = row.item_key.split(':comic_series:')[1] ?? row.item_key
seriesMap.set(row.item_key, {
id: idPart,
item_key: row.item_key,
title: row.title ?? decodeURIComponent(idPart),
coverUrl: meta.coverUrl ?? null,
issueCount: meta.issueCount ?? 0,
})
}
for (const row of allRows) {
if (row.item_type !== 'comic_issue') continue
const meta = row.metadata ? JSON.parse(row.metadata) : {}
const idPart = row.item_key.split(':comic_issue:')[1] ?? row.item_key
const issue: ComicIssue = {
id: idPart,
item_key: row.item_key,
title: row.title ?? decodeURIComponent(idPart.split(':').pop() ?? idPart),
issueNumber: meta.issueNumber ?? null,
pageCount: meta.pageCount ?? 0,
coverUrl: meta.coverUrl ?? null,
filePath: row.file_path ?? '',
isStandalone: meta.isStandalone ?? false,
}
if (row.parent_key && seriesMap.has(row.parent_key)) {
// Series issues are not included in the top-level grid — series card represents them
// We only include series cards + standalone issues in the grid
if (row.item_type === 'comic_series') {
const idPart = row.item_key.split(':comic_series:')[1] ?? row.item_key
items.push({
id: idPart,
item_key: row.item_key,
title: row.title ?? decodeURIComponent(idPart),
coverUrl: meta.coverUrl ?? null,
issueCount: meta.issueCount ?? 0,
} as ComicSeries)
} else {
standaloneIssues.push(issue)
const idPart = row.item_key.split(':comic_issue:')[1] ?? row.item_key
items.push({
id: idPart,
item_key: row.item_key,
title: row.title ?? decodeURIComponent(idPart.split(':').pop() ?? idPart),
issueNumber: meta.issueNumber ?? null,
pageCount: meta.pageCount ?? 0,
coverUrl: meta.coverUrl ?? null,
filePath: row.file_path ?? '',
isStandalone: meta.isStandalone ?? true,
} as ComicIssue)
}
}
const results: (ComicIssue | ComicSeries)[] = [
...Array.from(seriesMap.values()),
...standaloneIssues,
]
return results.sort((a, b) => naturalCompare(a.title, b.title))
return { items, total }
}
export function comicIssuesFromDb(libraryId: string, seriesId: string): ComicIssue[] {