605 lines
27 KiB
Swift
605 lines
27 KiB
Swift
import Foundation
|
|
|
|
final class RDEPUBMetadataParseWorker {
|
|
|
|
private static let maxRetryCount = 3
|
|
private static let retryDelays: [TimeInterval] = [0.5, 2.0, 8.0]
|
|
|
|
private final class ParseState {
|
|
|
|
var summariesBySpineIndex: [Int: RDEPUBChapterSummary]
|
|
|
|
var totalResolvedCount: Int
|
|
|
|
var lastAppliedCount: Int
|
|
|
|
init(
|
|
summariesBySpineIndex: [Int: RDEPUBChapterSummary],
|
|
totalResolvedCount: Int,
|
|
lastAppliedCount: Int
|
|
) {
|
|
self.summariesBySpineIndex = summariesBySpineIndex
|
|
self.totalResolvedCount = totalResolvedCount
|
|
self.lastAppliedCount = lastAppliedCount
|
|
}
|
|
}
|
|
|
|
private let backgroundInteractionCooldown: CFAbsoluteTime = 0.8
|
|
|
|
weak var context: RDEPUBReaderContext?
|
|
|
|
let cancellationController: RDEPUBMetadataParseCancellationController
|
|
|
|
let pageMapRefreshInterval: Int
|
|
|
|
private let token: UUID
|
|
|
|
private let parser: RDEPUBParser
|
|
|
|
private let publication: RDEPUBPublication
|
|
|
|
private let pageSize: CGSize
|
|
|
|
private let layoutConfig: RDEPUBTextLayoutConfig
|
|
|
|
private let style: RDEPUBTextRenderStyle
|
|
|
|
private let renderSignature: String
|
|
|
|
private let allBuildableIndices: [Int]
|
|
|
|
private let summaryDiskCache: RDEPUBChapterSummaryDiskCache?
|
|
|
|
private let workerCount: Int
|
|
|
|
private let contentHashBySpineIndex: [Int: String]
|
|
|
|
private let catalog: [(key: RDEPUBChapterCacheKey, spineIndex: Int, href: String, title: String)]
|
|
|
|
private let progressLogStride = 4
|
|
|
|
init(
|
|
context: RDEPUBReaderContext,
|
|
cancellationController: RDEPUBMetadataParseCancellationController,
|
|
token: UUID,
|
|
parser: RDEPUBParser,
|
|
publication: RDEPUBPublication
|
|
) {
|
|
self.context = context
|
|
self.cancellationController = cancellationController
|
|
self.token = token
|
|
self.parser = parser
|
|
self.publication = publication
|
|
|
|
let pageSize = context.currentTextPageSize()
|
|
self.pageSize = pageSize
|
|
self.layoutConfig = context.currentTextLayoutConfig(pageSize: pageSize)
|
|
self.style = context.currentTextRenderStyle()
|
|
self.renderSignature = context.currentRenderSignature()
|
|
self.allBuildableIndices = publication.spine.indices.filter { index in
|
|
guard publication.spine.indices.contains(index) else { return false }
|
|
let item = publication.spine[index]
|
|
return item.linear && (item.mediaType.contains("html") || item.mediaType.contains("xhtml"))
|
|
}
|
|
self.summaryDiskCache = context.runtime?.summaryDiskCache
|
|
self.workerCount = max(1, context.configuration.metadataParsingConcurrency)
|
|
self.pageMapRefreshInterval = RDEPUBReaderPaginationCoordinator.pageMapRefreshInterval
|
|
|
|
var hashes: [Int: String] = [:]
|
|
for spineIndex in allBuildableIndices {
|
|
guard let href = publication.spine.indices.contains(spineIndex)
|
|
? publication.spine[spineIndex].href : nil,
|
|
let html = parser.htmlString(forRelativePath: href) else {
|
|
hashes[spineIndex] = ""
|
|
continue
|
|
}
|
|
hashes[spineIndex] = html.rd_sha256Hex
|
|
}
|
|
self.contentHashBySpineIndex = hashes
|
|
|
|
let ctx = context
|
|
let spine = publication.spine
|
|
let sig = renderSignature
|
|
self.catalog = allBuildableIndices.map { spineIndex in
|
|
let item = spine[spineIndex]
|
|
return (
|
|
key: ctx.chapterCacheKey(
|
|
forSpineIndex: spineIndex,
|
|
precomputedContentHash: hashes[spineIndex] ?? "",
|
|
renderSignature: sig
|
|
),
|
|
spineIndex: spineIndex,
|
|
href: item.href,
|
|
title: item.title
|
|
)
|
|
}
|
|
}
|
|
|
|
func start(token: UUID, restoreLocation: RDEPUBLocation?) {
|
|
let cancellationController = self.cancellationController
|
|
|
|
DispatchQueue.global(qos: .utility).async { [self] in
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"workerDispatched token=\(token.uuidString)"
|
|
)
|
|
let context = self.context
|
|
guard let context,
|
|
context.controller != nil,
|
|
!cancellationController.isCancelled,
|
|
context.paginationToken == token else {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"workerAbortedBeforeStart reason=contextUnavailableOrTokenMismatch"
|
|
)
|
|
return
|
|
}
|
|
defer { context.runtime?.paginationCoordinator.finishMetadataParseCancellationController(cancellationController) }
|
|
guard context.controller != nil,
|
|
!cancellationController.isCancelled,
|
|
context.paginationToken == token else {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"workerAbortedAfterStart reason=contextUnavailableOrTokenMismatch"
|
|
)
|
|
return
|
|
}
|
|
|
|
if let restoredPageMap = self.restoreBookPageMapIfPossible() {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"restoreBookPageMapIfPossible hit totalChapters=\(restoredPageMap.totalChapters) totalPages=\(restoredPageMap.totalPages)"
|
|
)
|
|
DispatchQueue.main.async {
|
|
guard context.paginationToken == token,
|
|
context.controller != nil,
|
|
!cancellationController.isCancelled else { return }
|
|
context.runtime?.refreshBookPageMapInPlace(restoredPageMap)
|
|
}
|
|
return
|
|
}
|
|
|
|
let prewarmStart = CFAbsoluteTimeGetCurrent()
|
|
let prewarmMs = Int((CFAbsoluteTimeGetCurrent() - prewarmStart) * 1000)
|
|
|
|
let restored = self.summaryDiskCache?.readAll(keys: self.catalog)
|
|
let cachedSummaries = restored?.summaries ?? [:]
|
|
let cachedSpineIndices = Set(cachedSummaries.keys)
|
|
let resultLock = NSLock()
|
|
let parseState = ParseState(
|
|
summariesBySpineIndex: cachedSummaries,
|
|
totalResolvedCount: cachedSpineIndices.count,
|
|
lastAppliedCount: cachedSpineIndices.count
|
|
)
|
|
|
|
if !cachedSpineIndices.isEmpty {
|
|
let cachedMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
|
|
DispatchQueue.main.async {
|
|
guard context.paginationToken == token,
|
|
context.controller != nil else { return }
|
|
context.runtime?.refreshBookPageMapInPlace(cachedMap)
|
|
}
|
|
}
|
|
|
|
let prioritizedSpineIndices: [Int]
|
|
if let priorityManager = context.runtime?.backgroundPriorityManager {
|
|
let currentSpineIndex = context.runtime?.locationCoordinator.currentVisibleLocation()
|
|
.flatMap { context.normalizedSpineIndex(for: $0) }
|
|
prioritizedSpineIndices = priorityManager.makeMetadataPriorityOrder(
|
|
allBuildableIndices: self.allBuildableIndices,
|
|
currentSpineIndex: currentSpineIndex,
|
|
cachedSpineIndices: cachedSpineIndices
|
|
)
|
|
} else {
|
|
prioritizedSpineIndices = self.allBuildableIndices.filter { !cachedSpineIndices.contains($0) }
|
|
}
|
|
|
|
let uncachedSpineIndices = prioritizedSpineIndices
|
|
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"waitingForInteractionCooldown elapsedSinceNavigation=\(String(format: "%.2f", context.secondsSinceLastUserNavigation())) uncached=\(uncachedSpineIndices.count)"
|
|
)
|
|
self.waitForReadingInteractionToSettle(cancellationController: cancellationController)
|
|
guard !cancellationController.isCancelled,
|
|
context.controller != nil,
|
|
context.paginationToken == token else {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"workerAbortedDuringCooldown"
|
|
)
|
|
return
|
|
}
|
|
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"start totalBuildable=\(self.allBuildableIndices.count) cached=\(cachedSpineIndices.count) uncached=\(uncachedSpineIndices.count) concurrency=\(self.workerCount) refreshInterval=\(self.pageMapRefreshInterval)"
|
|
)
|
|
|
|
let wallClockStart = CFAbsoluteTimeGetCurrent()
|
|
var totalRenderMs: Double = 0
|
|
var totalWriteMs: Double = 0
|
|
var totalMergeMs: Double = 0
|
|
var completedChapters = 0
|
|
var failedChapters = 0
|
|
let timingLock = NSLock()
|
|
|
|
let queue = OperationQueue()
|
|
queue.name = "com.rdreader.metadata.parse"
|
|
queue.qualityOfService = .utility
|
|
queue.maxConcurrentOperationCount = self.workerCount
|
|
cancellationController.attach(queue: queue)
|
|
|
|
let refreshInterval = self.pageMapRefreshInterval
|
|
|
|
for (offset, spineIndex) in uncachedSpineIndices.enumerated() {
|
|
let operation = BlockOperation()
|
|
operation.addExecutionBlock { [weak operation] in
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled,
|
|
operation?.isCancelled != true else {
|
|
return
|
|
}
|
|
do {
|
|
|
|
let renderResult: RDEPUBChapterSummary? = try autoreleasepool { () -> RDEPUBChapterSummary? in
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled,
|
|
operation?.isCancelled != true else {
|
|
return nil
|
|
}
|
|
let chapterBuilder = context.makeTextBookBuilder(layoutConfig: self.layoutConfig)
|
|
let renderStart = CFAbsoluteTimeGetCurrent()
|
|
guard let result = try chapterBuilder.buildChapter(
|
|
parser: self.parser,
|
|
publication: self.publication,
|
|
spineIndex: spineIndex,
|
|
pageSize: self.pageSize,
|
|
style: self.style
|
|
) else {
|
|
return nil
|
|
}
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled,
|
|
operation?.isCancelled != true else {
|
|
return nil
|
|
}
|
|
let renderElapsed = (CFAbsoluteTimeGetCurrent() - renderStart) * 1000
|
|
|
|
let chapter = result.chapter
|
|
let precomputedHash = self.contentHashBySpineIndex[spineIndex] ?? ""
|
|
let cacheKey = context.chapterCacheKey(
|
|
forSpineIndex: spineIndex,
|
|
precomputedContentHash: precomputedHash,
|
|
renderSignature: self.renderSignature
|
|
)
|
|
let summary = RDEPUBChapterSummary(
|
|
pageRanges: chapter.pages.map { .init(location: $0.contentRange.location, length: $0.contentRange.length) },
|
|
pageCount: chapter.pages.count,
|
|
fragmentOffsets: chapter.fragmentOffsets,
|
|
cfiMap: chapter.cfiMap,
|
|
renderSignature: cacheKey.renderSignature,
|
|
schemaVersion: RDEPUBChapterSummary.currentSchemaVersion,
|
|
chapterContentHash: cacheKey.chapterContentHash,
|
|
pageMetadataList: chapter.pages.map { .from($0.metadata) }
|
|
)
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled,
|
|
operation?.isCancelled != true else {
|
|
return nil
|
|
}
|
|
let writeStart = CFAbsoluteTimeGetCurrent()
|
|
self.summaryDiskCache?.write(summary: summary, for: cacheKey)
|
|
let writeElapsed = (CFAbsoluteTimeGetCurrent() - writeStart) * 1000
|
|
|
|
timingLock.lock()
|
|
totalRenderMs += renderElapsed
|
|
totalWriteMs += writeElapsed
|
|
completedChapters += 1
|
|
timingLock.unlock()
|
|
|
|
return summary
|
|
}
|
|
|
|
guard let renderResult else { return }
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled,
|
|
operation?.isCancelled != true else {
|
|
return
|
|
}
|
|
|
|
var snapshot: [Int: RDEPUBChapterSummary]?
|
|
resultLock.lock()
|
|
parseState.summariesBySpineIndex[spineIndex] = renderResult
|
|
parseState.totalResolvedCount += 1
|
|
let resolvedCount = parseState.totalResolvedCount
|
|
let shouldLogProgress = resolvedCount == self.allBuildableIndices.count
|
|
|| resolvedCount == cachedSpineIndices.count + 1
|
|
|| resolvedCount % self.progressLogStride == 0
|
|
if parseState.totalResolvedCount - parseState.lastAppliedCount >= refreshInterval
|
|
|| parseState.totalResolvedCount == self.allBuildableIndices.count {
|
|
parseState.lastAppliedCount = parseState.totalResolvedCount
|
|
snapshot = parseState.summariesBySpineIndex
|
|
}
|
|
resultLock.unlock()
|
|
|
|
if shouldLogProgress {
|
|
let progressPercent = Self.progressPercent(
|
|
resolved: resolvedCount,
|
|
total: self.allBuildableIndices.count
|
|
)
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"chapterReady spine=\(spineIndex) resolved=\(resolvedCount)/\(self.allBuildableIndices.count) progress=\(progressPercent)% pageCount=\(renderResult.pageCount)"
|
|
)
|
|
}
|
|
|
|
if let snapshot {
|
|
let mergeStart = CFAbsoluteTimeGetCurrent()
|
|
let partialMap = self.buildPageMap(summaries: snapshot)
|
|
let mergeElapsed = (CFAbsoluteTimeGetCurrent() - mergeStart) * 1000
|
|
timingLock.lock()
|
|
totalMergeMs += mergeElapsed
|
|
timingLock.unlock()
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"partialMap resolvedChapters=\(snapshot.count) totalPages=\(partialMap.totalPages) mergeMs=\(Int(mergeElapsed))"
|
|
)
|
|
DispatchQueue.main.async {
|
|
guard context.paginationToken == token,
|
|
context.controller != nil,
|
|
!cancellationController.isCancelled else { return }
|
|
context.runtime?.refreshBookPageMapInPlace(partialMap)
|
|
}
|
|
}
|
|
} catch {
|
|
guard !cancellationController.isCancelled,
|
|
context.paginationToken == token,
|
|
context.controller != nil,
|
|
operation?.isCancelled != true else {
|
|
return
|
|
}
|
|
timingLock.lock()
|
|
failedChapters += 1
|
|
timingLock.unlock()
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"chapterFailed spine=\(spineIndex) retryScheduled=true error=\(String(describing: error))"
|
|
)
|
|
|
|
self.scheduleRetry(
|
|
spineIndex: spineIndex,
|
|
retryCount: 0,
|
|
resultLock: resultLock,
|
|
parseState: parseState,
|
|
cancellationController: cancellationController
|
|
)
|
|
}
|
|
}
|
|
queue.addOperation(operation)
|
|
}
|
|
queue.waitUntilAllOperationsAreFinished()
|
|
if !cancellationController.isCancelled,
|
|
context.paginationToken == token,
|
|
context.controller != nil {
|
|
self.summaryDiskCache?.flushPendingWrites()
|
|
}
|
|
|
|
let wallClockMs = Int((CFAbsoluteTimeGetCurrent() - wallClockStart) * 1000)
|
|
timingLock.lock()
|
|
let renderTotal = Int(totalRenderMs)
|
|
let writeTotal = Int(totalWriteMs)
|
|
let mergeTotal = Int(totalMergeMs)
|
|
let rendered = completedChapters
|
|
let failed = failedChapters
|
|
timingLock.unlock()
|
|
let avgRenderMs = rendered > 0 ? renderTotal / rendered : 0
|
|
context.lastMetadataParseWallClockMs = wallClockMs
|
|
context.lastMetadataParseConcurrency = self.workerCount
|
|
|
|
guard context.controller != nil,
|
|
context.paginationToken == token,
|
|
!cancellationController.isCancelled else {
|
|
return
|
|
}
|
|
|
|
let finalMergeStart = CFAbsoluteTimeGetCurrent()
|
|
let pageMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
|
|
let finalMergeMs = Int((CFAbsoluteTimeGetCurrent() - finalMergeStart) * 1000)
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"finish resolved=\(parseState.summariesBySpineIndex.count)/\(self.allBuildableIndices.count) totalPages=\(pageMap.totalPages) elapsedMs=\(wallClockMs) renderMs=\(renderTotal) writeMs=\(writeTotal) mergeMs=\(mergeTotal + finalMergeMs) failed=\(failed)"
|
|
)
|
|
|
|
if let coverageStore = context.runtime?.backgroundCoverageStore {
|
|
let resolvedSpineIndices = Set(parseState.summariesBySpineIndex.keys)
|
|
let lowerSpine = resolvedSpineIndices.min() ?? 0
|
|
let upperSpine = resolvedSpineIndices.max() ?? 0
|
|
let estimatedBytes = 256 + pageMap.entries.count * 96 + resolvedSpineIndices.count * 16
|
|
|
|
let segment = RDEPUBBackgroundCoverageSegment(
|
|
lowerSpineIndex: lowerSpine,
|
|
upperSpineIndex: upperSpine,
|
|
pageMap: pageMap,
|
|
resolvedSpineIndices: resolvedSpineIndices,
|
|
generatedAt: CFAbsoluteTimeGetCurrent(),
|
|
renderSignature: self.renderSignature,
|
|
estimatedMemoryBytes: estimatedBytes
|
|
)
|
|
coverageStore.addSegment(segment)
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
guard context.paginationToken == token,
|
|
context.controller != nil else { return }
|
|
context.runtime?.refreshBookPageMapInPlace(pageMap)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Private
|
|
|
|
private func waitForReadingInteractionToSettle(
|
|
cancellationController: RDEPUBMetadataParseCancellationController? = nil
|
|
) {
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
let maxWaitIterations = 100 // ~8 seconds max wait
|
|
var iteration = 0
|
|
while context?.controller != nil,
|
|
cancellationController?.isCancelled != true,
|
|
iteration < maxWaitIterations {
|
|
let elapsed = context?.secondsSinceLastUserNavigation() ?? 0
|
|
if elapsed >= backgroundInteractionCooldown { break }
|
|
semaphore.wait(timeout: .now() + 0.08)
|
|
iteration += 1
|
|
}
|
|
}
|
|
|
|
private func scheduleRetry(
|
|
spineIndex: Int,
|
|
retryCount: Int,
|
|
resultLock: NSLock,
|
|
parseState: ParseState,
|
|
cancellationController: RDEPUBMetadataParseCancellationController
|
|
) {
|
|
guard retryCount < Self.maxRetryCount else {
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"retryAborted spine=\(spineIndex) retryCount=\(retryCount)"
|
|
)
|
|
return
|
|
}
|
|
|
|
let delay = Self.retryDelays[min(retryCount, Self.retryDelays.count - 1)]
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"retryScheduled spine=\(spineIndex) retryCount=\(retryCount + 1) delayMs=\(Int(delay * 1000))"
|
|
)
|
|
|
|
DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
guard let self, let context = self.context else { return }
|
|
guard context.controller != nil,
|
|
context.paginationToken == self.token,
|
|
!cancellationController.isCancelled else {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let chapterBuilder = context.makeTextBookBuilder(layoutConfig: self.layoutConfig)
|
|
guard let result = try chapterBuilder.buildChapter(
|
|
parser: self.parser,
|
|
publication: self.publication,
|
|
spineIndex: spineIndex,
|
|
pageSize: self.pageSize,
|
|
style: self.style
|
|
) else {
|
|
return
|
|
}
|
|
guard context.controller != nil,
|
|
context.paginationToken == self.token,
|
|
!cancellationController.isCancelled else {
|
|
return
|
|
}
|
|
|
|
let chapter = result.chapter
|
|
let precomputedHash = self.contentHashBySpineIndex[spineIndex] ?? ""
|
|
let cacheKey = context.chapterCacheKey(
|
|
forSpineIndex: spineIndex,
|
|
precomputedContentHash: precomputedHash,
|
|
renderSignature: self.renderSignature
|
|
)
|
|
let summary = RDEPUBChapterSummary(
|
|
pageRanges: chapter.pages.map { .init(location: $0.contentRange.location, length: $0.contentRange.length) },
|
|
pageCount: chapter.pages.count,
|
|
fragmentOffsets: chapter.fragmentOffsets,
|
|
cfiMap: chapter.cfiMap,
|
|
renderSignature: cacheKey.renderSignature,
|
|
schemaVersion: RDEPUBChapterSummary.currentSchemaVersion,
|
|
chapterContentHash: cacheKey.chapterContentHash,
|
|
pageMetadataList: chapter.pages.map { .from($0.metadata) }
|
|
)
|
|
guard context.controller != nil,
|
|
context.paginationToken == self.token,
|
|
!cancellationController.isCancelled else {
|
|
return
|
|
}
|
|
self.summaryDiskCache?.write(summary: summary, for: cacheKey)
|
|
|
|
resultLock.lock()
|
|
parseState.summariesBySpineIndex[spineIndex] = summary
|
|
parseState.totalResolvedCount += 1
|
|
let shouldRefresh =
|
|
parseState.totalResolvedCount - parseState.lastAppliedCount >= self.pageMapRefreshInterval
|
|
|| parseState.totalResolvedCount == self.allBuildableIndices.count
|
|
if shouldRefresh {
|
|
parseState.lastAppliedCount = parseState.totalResolvedCount
|
|
}
|
|
resultLock.unlock()
|
|
|
|
if shouldRefresh {
|
|
let partialMap = self.buildPageMap(summaries: parseState.summariesBySpineIndex)
|
|
RDEPUBBackgroundTrace.log(
|
|
"Metadata",
|
|
"retryPartialMap resolvedChapters=\(parseState.summariesBySpineIndex.count) totalPages=\(partialMap.totalPages)"
|
|
)
|
|
DispatchQueue.main.async {
|
|
guard context.paginationToken == self.token,
|
|
context.controller != nil,
|
|
!cancellationController.isCancelled else { return }
|
|
context.runtime?.refreshBookPageMapInPlace(partialMap)
|
|
}
|
|
}
|
|
|
|
} catch {
|
|
|
|
self.scheduleRetry(
|
|
spineIndex: spineIndex,
|
|
retryCount: retryCount + 1,
|
|
resultLock: resultLock,
|
|
parseState: parseState,
|
|
cancellationController: cancellationController
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func progressPercent(resolved: Int, total: Int) -> Int {
|
|
guard total > 0 else { return 0 }
|
|
return Int((Double(resolved) / Double(total) * 100.0).rounded())
|
|
}
|
|
|
|
private func restoreBookPageMapIfPossible() -> RDEPUBBookPageMap? {
|
|
guard let summaryDiskCache else { return nil }
|
|
guard summaryDiskCache.isCacheComplete(keys: catalog.map(\.key)) else {
|
|
return nil
|
|
}
|
|
let restored = summaryDiskCache.readAll(keys: catalog)
|
|
guard restored.summaries.count == catalog.count else {
|
|
return nil
|
|
}
|
|
return restored.mapBuilder.build()
|
|
}
|
|
|
|
private func buildPageMap(
|
|
summaries: [Int: RDEPUBChapterSummary]
|
|
) -> RDEPUBBookPageMap {
|
|
var builder = RDEPUBBookPageMap.Builder()
|
|
for item in catalog {
|
|
guard let summary = summaries[item.spineIndex] else { continue }
|
|
builder.add(
|
|
spineIndex: item.spineIndex,
|
|
href: item.href,
|
|
title: item.title,
|
|
pageCount: summary.pageCount,
|
|
fragmentOffsets: summary.fragmentOffsets
|
|
)
|
|
}
|
|
return builder.build()
|
|
}
|
|
}
|