mirror of
https://github.com/bookorbit/bookorbit.git
synced 2026-11-03 00:11:37 -06:00
feat(books): add read-aloud delivery policies and preserve sync
Add original and text-only EPUB delivery choices across Kobo, KOReader, OPDS, web downloads and email while keeping source files unchanged. Persist server defaults, device overrides and captured artifact identities so retries and guided replacement retain the selected rendition. Preserve saved progress across first opens, replacement resets and stale updates, and project annotations against the copy delivered to each device. Handle XML resource references, large inodes, retry backoff and background worker state correctly. Consolidate the schema changes into one migration and retain released-client request and response compatibility. Verify with the full server and client checks, delivery and sync HTTP suites, independent code reviews, and live KOReader download/settings/reader proof. Fixes #1496 Closes #1603 Closes #1714 Fixes #1723 Ref #1633 Ref #1508 Ref #1621 Ref #1136
This commit is contained in:
@@ -15,6 +15,8 @@ export const SERVER_FEATURES = [
|
||||
"book-request-pairs",
|
||||
/** Metadata searches (book and author) accept `cache=refresh` and mark candidates answered from the cache. */
|
||||
"metadata-search-cache",
|
||||
"read-aloud-delivery",
|
||||
"koreader-delivery-settings",
|
||||
] as const;
|
||||
|
||||
export type ServerFeature = (typeof SERVER_FEATURES)[number];
|
||||
|
||||
@@ -223,6 +223,8 @@ export type BookCard = {
|
||||
};
|
||||
|
||||
export type BookDetailFile = {
|
||||
fileHash?: string | null;
|
||||
contentVersion?: string;
|
||||
id: number;
|
||||
format: string | null;
|
||||
role: string;
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const READ_ALOUD_DELIVERY_VARIANTS = ["original", "audioless_epub"] as const;
|
||||
export type ReadAloudDeliveryVariant = (typeof READ_ALOUD_DELIVERY_VARIANTS)[number];
|
||||
export type ReadAloudDeliveryPreference = ReadAloudDeliveryVariant | null;
|
||||
|
||||
export interface DeliveryArtifactMetadata {
|
||||
artifactId: string;
|
||||
sourceRevision: string;
|
||||
requestedVariant: ReadAloudDeliveryPreference;
|
||||
actualVariant: ReadAloudDeliveryVariant;
|
||||
format: string;
|
||||
sizeBytes: number;
|
||||
sha256: string;
|
||||
fileHash: string;
|
||||
outcome: "original" | "verified_plain" | "stripped" | "legacy_fallback";
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { DeliveryArtifactMetadata, ReadAloudDeliveryPreference } from "./delivery";
|
||||
|
||||
export interface EmailRecipient {
|
||||
id: number;
|
||||
userId: number;
|
||||
name: string;
|
||||
email: string;
|
||||
isDefault: boolean;
|
||||
deviceType: string | null;
|
||||
preferredFormat: string | null;
|
||||
defaultTemplateId: number | null;
|
||||
createdAt: string;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
}
|
||||
|
||||
export interface EmailRecipientForm {
|
||||
name: string;
|
||||
email: string;
|
||||
deviceType: string | null;
|
||||
preferredFormat: string | null;
|
||||
defaultTemplateId: number | null;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
}
|
||||
|
||||
export interface EmailSendLogEntry {
|
||||
id: number;
|
||||
userId: number;
|
||||
bookId: number | null;
|
||||
bookFileId: number | null;
|
||||
providerId: number | null;
|
||||
templateId: number | null;
|
||||
toEmail: string;
|
||||
toName: string | null;
|
||||
subject: string | null;
|
||||
status: "pending" | "sent" | "failed";
|
||||
attemptCount: number;
|
||||
errorMessage: string | null;
|
||||
sentAt: string | null;
|
||||
createdAt: string;
|
||||
deliveryPolicy?: ReadAloudDeliveryPreference;
|
||||
sourceRevision?: string | null;
|
||||
actualVariant?: string | null;
|
||||
artifactId?: string | null;
|
||||
deviceType?: string | null;
|
||||
recipientId?: number | null;
|
||||
failureCode?: string | null;
|
||||
dispatchPhase?: string | null;
|
||||
deliveryMetadata?: DeliveryArtifactMetadata | null;
|
||||
}
|
||||
@@ -86,3 +86,5 @@ export * from "./cron";
|
||||
export * from "./tts";
|
||||
export * from "./watch-download";
|
||||
export * from "./podcast";
|
||||
export * from "./delivery";
|
||||
export * from "./email";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { ReadAloudDeliveryPreference } from "./delivery";
|
||||
|
||||
export interface KoboDevice {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -17,6 +19,7 @@ export interface KoboSyncSettings {
|
||||
kepubConversionLimitMb: number;
|
||||
twoWayProgressSync: boolean;
|
||||
syncBookOrbitAnnotationsToKobo: boolean;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
storeSync: boolean;
|
||||
}
|
||||
|
||||
@@ -55,5 +58,6 @@ export interface UpdateKoboSyncSettingsRequest {
|
||||
kepubConversionLimitMb?: number;
|
||||
twoWayProgressSync?: boolean;
|
||||
syncBookOrbitAnnotationsToKobo?: boolean;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
storeSync?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { ReadAloudDeliveryPreference, ReadAloudDeliveryVariant, DeliveryArtifactMetadata } from "./delivery";
|
||||
import type { HighlightOfTheDayWidgetData, ReadingGoalWidgetData, ReadingStreakWidgetData } from "./dashboard";
|
||||
import type { SeriesIndex } from "./series-index";
|
||||
|
||||
export interface KoreaderCredentials {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
username: string;
|
||||
syncEnabled: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface KoreaderDeviceInfo {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
device: string;
|
||||
deviceId: string;
|
||||
lastSyncAt: string;
|
||||
@@ -29,6 +32,7 @@ export interface KoreaderBookProgress {
|
||||
}
|
||||
|
||||
export interface KoreaderDeviceSweepInfo {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
deviceId: string;
|
||||
deviceModel: string;
|
||||
pluginVersion: string | null;
|
||||
@@ -144,11 +148,13 @@ export interface UnlinkKoreaderManualHashLinkResult {
|
||||
}
|
||||
|
||||
export interface CreateKoreaderCredentialsPayload {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface UpdateKoreaderCredentialsPayload {
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
username?: string;
|
||||
password?: string;
|
||||
syncEnabled?: boolean;
|
||||
@@ -193,6 +199,13 @@ export interface KoreaderCatalogEntry {
|
||||
}
|
||||
|
||||
export interface KoreaderCatalogFile {
|
||||
fileHash?: string | null;
|
||||
sourceMediaOverlayAvailable?: boolean;
|
||||
narrationPlaybackAvailable?: boolean;
|
||||
selection?: string;
|
||||
delivery?: DeliveryArtifactMetadata;
|
||||
requestedVariant?: "original" | "audioless_epub";
|
||||
contentVersion?: string;
|
||||
id: number;
|
||||
format: string;
|
||||
role: string;
|
||||
@@ -367,6 +380,11 @@ export interface KoreaderCatalogDashboardSectionResponse {
|
||||
// download route streams, so it doubles as the restart-validation checksum and
|
||||
// as the digest that keys local match state.
|
||||
export interface KoreaderCatalogManifestFile {
|
||||
selection?: string;
|
||||
delivery?: DeliveryArtifactMetadata;
|
||||
requestedVariant?: "original" | "audioless_epub";
|
||||
sourceMediaOverlayAvailable?: boolean;
|
||||
narrationPlaybackAvailable?: boolean;
|
||||
id: number;
|
||||
format: string;
|
||||
downloadVariant: "original" | "audioless_epub";
|
||||
@@ -401,7 +419,8 @@ export interface KoreaderCatalogManifestPage {
|
||||
restartRequired: boolean;
|
||||
}
|
||||
|
||||
export type KoreaderPluginCapability = "catalogBulkManifest" | "catalogDashboardSections" | "bookmarkSync";
|
||||
export type KoreaderPluginCapability =
|
||||
"catalogBulkManifest" | "catalogDashboardSections" | "bookmarkSync" | "readAloudDeliveryVariants" | "readAloudDeliverySettings";
|
||||
|
||||
export interface KoreaderPluginVersionInfo {
|
||||
pluginVersion: string;
|
||||
@@ -416,3 +435,14 @@ export interface KoreaderCatalogReadStatusResult {
|
||||
export interface KoreaderCatalogRatingResult {
|
||||
rating: number | null;
|
||||
}
|
||||
|
||||
export interface KoreaderDeliverySettings {
|
||||
readAloudDelivery: ReadAloudDeliveryVariant;
|
||||
credentialDefault: ReadAloudDeliveryPreference;
|
||||
deviceOverride: ReadAloudDeliveryPreference;
|
||||
deviceConfigured: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateKoreaderDeliverySettingsPayload {
|
||||
readAloudDelivery: ReadAloudDeliveryPreference;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { ReadAloudDeliveryPreference } from "./delivery";
|
||||
|
||||
export type OpdsSortOrder = "recent" | "title_asc" | "title_desc" | "author_asc" | "author_desc" | "series_asc" | "series_desc";
|
||||
|
||||
export interface OpdsUser {
|
||||
id: number;
|
||||
userId: number;
|
||||
username: string;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
sortOrder: OpdsSortOrder;
|
||||
createdAt: string;
|
||||
}
|
||||
@@ -11,9 +14,11 @@ export interface OpdsUser {
|
||||
export interface CreateOpdsUserRequest {
|
||||
username: string;
|
||||
password: string;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
sortOrder?: OpdsSortOrder;
|
||||
}
|
||||
|
||||
export interface UpdateOpdsUserRequest {
|
||||
sortOrder: OpdsSortOrder;
|
||||
readAloudDelivery?: ReadAloudDeliveryPreference;
|
||||
sortOrder?: OpdsSortOrder;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user