From aed22712ccf25736c3abe76f285925f563e67196 Mon Sep 17 00:00:00 2001
From: Chris Mansell <213777310+chrismansell26@users.noreply.github.com>
Date: Sat, 13 Jun 2026 11:40:53 -0600
Subject: [PATCH] feat(metadata): add LubimyCzytac metadata provider (#324)
* feat(metadata): add LubimyCzytac metadata provider
- Add LubimyCzytac scraper and provider implementation
- Add lubimyczytacId field across types, schema, extractors, and metadata service
- Register lubimyczytac provider in module, pipeline, repository, and config service
- Add provider icon, color, link, and UI support in book detail views
- Add migration for lubimyczytac_id column
Closes #304
* fix(books): keep cover card read button opening reader when thumbnail click prefers details
The overlay primary button should always open the reader regardless of
the thumbnailClickAction setting. That setting only controls card click
behaviour.
* fix(books): add explicit read button to hover overlay when thumbnail click prefers details
When thumbnailClickAction is 'details', a dedicated Read button now
appears in the overlay top row so users can still open the reader
directly without changing their preference.
---
.../assets/provider-icons/lubimyczytac.svg | 9 +
.../book/components/BookCoverCard.vue | 16 +-
.../book/components/BookQuickView.vue | 10 +
.../components/detail/tabs/DetailsTab.vue | 10 +
.../detail/tabs/EditMetadataTab.vue | 3 +
.../book/composables/useFileMetadata.ts | 1 +
.../book/composables/useMetadataDiff.ts | 4 +
.../book/composables/useMetadataEditor.ts | 3 +
.../features/book/lib/file-metadata-patch.ts | 1 +
.../book/lib/metadata-refresh-feedback.ts | 1 +
.../src/features/book/lib/provider-icons.ts | 1 +
.../src/features/book/lib/provider-links.ts | 7 +
.../components/ProviderConfigPanel.vue | 6 +
client/src/lib/provider-colors.ts | 2 +
packages/types/src/book.ts | 1 +
packages/types/src/file-write.ts | 2 +
packages/types/src/metadata-fetch.ts | 1 +
packages/types/src/metadata-lock.ts | 15 +-
packages/types/src/metadata-preferences.ts | 1 +
.../0019_add_lubimyczytac_provider_id.sql | 1 +
.../src/db/migrations/meta/0019_snapshot.json | 10767 ++++++++++++++++
server/src/db/migrations/meta/_journal.json | 7 +
server/src/db/schema/metadata.ts | 1 +
...ook-metadata-fetch-orchestrator.service.ts | 3 +
.../book-metadata-lock.service.ts | 3 +
server/src/modules/book/book.service.ts | 9 +
.../book/dto/update-book-metadata.dto.ts | 1 +
.../providers/itunes-cover-provider.test.ts | 1 +
.../file-write/file-write.constants.ts | 2 +
.../file-write/file-write.repository.ts | 1 +
.../book-write-payload.interface.ts | 1 +
.../metadata-fetch-pipeline.test.ts | 2 +
.../metadata-fetch.controller.test.ts | 2 +
.../metadata-fetch.module.test.ts | 3 +
.../metadata-fetch/metadata-fetch.module.ts | 2 +
.../metadata-fetch.repository.ts | 2 +
.../metadata-fetch/metadata-fetch.service.ts | 1 +
.../providers/amazon/amazon.provider.test.ts | 1 +
.../audible/audible.provider.test.ts | 1 +
.../audnexus/audnexus.provider.test.ts | 1 +
.../comicvine/comicvine.provider.test.ts | 1 +
.../goodreads/goodreads.provider.test.ts | 1 +
.../providers/google/google.provider.test.ts | 1 +
.../hardcover/hardcover.provider.test.ts | 1 +
.../providers/itunes/itunes.provider.test.ts | 1 +
.../providers/kobo/kobo.provider.test.ts | 2 +
.../lubimyczytac.provider.test.ts | 216 +
.../lubimyczytac/lubimyczytac.provider.ts | 144 +
.../lubimyczytac/lubimyczytac.scraper.test.ts | 439 +
.../lubimyczytac/lubimyczytac.scraper.ts | 345 +
.../open-library.provider.test.ts | 1 +
.../providers/provider-constants.ts | 2 +
.../ranobedb/ranobedb.provider.test.ts | 1 +
.../dto/update-provider-config.dto.ts | 1 +
.../provider-config.service.test.ts | 4 +
.../provider-config.service.ts | 11 +
.../extractors/comic-format.extractor.ts | 1 +
.../extractors/format-extractor.interface.ts | 1 +
.../extractors/opf-metadata.mapper.ts | 2 +
.../extractors/pdf-format.extractor.ts | 1 +
.../src/modules/metadata/lib/cbz-metadata.ts | 3 +
server/src/modules/metadata/lib/opf-parser.ts | 7 +
server/src/modules/metadata/lib/pdf-parser.ts | 2 +
.../modules/metadata/lib/pdf-xmp-reader.ts | 2 +
.../src/modules/metadata/metadata.service.ts | 2 +
.../statistics/statistics.repository.ts | 3 +-
66 files changed, 12097 insertions(+), 4 deletions(-)
create mode 100644 client/public/assets/provider-icons/lubimyczytac.svg
create mode 100644 client/src/features/book/lib/provider-links.ts
create mode 100644 server/src/db/migrations/0019_add_lubimyczytac_provider_id.sql
create mode 100644 server/src/db/migrations/meta/0019_snapshot.json
create mode 100644 server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.test.ts
create mode 100644 server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts
create mode 100644 server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.test.ts
create mode 100644 server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.ts
diff --git a/client/public/assets/provider-icons/lubimyczytac.svg b/client/public/assets/provider-icons/lubimyczytac.svg
new file mode 100644
index 000000000..7a8e0a80c
--- /dev/null
+++ b/client/public/assets/provider-icons/lubimyczytac.svg
@@ -0,0 +1,9 @@
+
+ LubimyCzytac
+
+
+
+
+
+
+
diff --git a/client/src/features/book/components/BookCoverCard.vue b/client/src/features/book/components/BookCoverCard.vue
index cf5894915..ade11be92 100644
--- a/client/src/features/book/components/BookCoverCard.vue
+++ b/client/src/features/book/components/BookCoverCard.vue
@@ -261,6 +261,7 @@ const primaryOverlayActionIcon = computed(() => {
return isAudiobook.value ? Play : BookOpen
})
const primaryOverlayActionIconClass = computed(() => (thumbnailClickAction.value !== 'details' && isAudiobook.value ? 'ml-[2cqi]' : ''))
+const showExplicitReadButton = computed(() => thumbnailClickAction.value === 'details' && primaryFile.value != null && !isMissing.value)
function handlePrimaryOverlayAction() {
if (thumbnailClickAction.value === 'details') {
@@ -271,6 +272,10 @@ function handlePrimaryOverlayAction() {
if (primaryFile.value && !isMissing.value) openFile(primaryFile.value)
}
+function openPrimaryFileExplicit() {
+ if (primaryFile.value && !isMissing.value) openFile(primaryFile.value)
+}
+
function handleCardClick(event: MouseEvent) {
const target = event.target
if (target instanceof Element && target.closest('button, [data-card-click-blocker]')) return
@@ -517,11 +522,18 @@ const secondaryLabelText = computed(() => resolveBookLabel(gridCardSecondaryLabe
showMobileOverlay ? 'opacity-100' : 'opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto',
]"
>
-
-
+
+
diff --git a/client/src/features/book/components/BookQuickView.vue b/client/src/features/book/components/BookQuickView.vue
index 381547215..5f3a8cd62 100644
--- a/client/src/features/book/components/BookQuickView.vue
+++ b/client/src/features/book/components/BookQuickView.vue
@@ -9,6 +9,7 @@ import { DialogRoot, DialogContent, DialogPortal, DialogOverlay, DialogClose, Di
import { formatBytes } from '@/lib/formatting'
import { getProviderColor } from '@/lib/provider-colors'
import { providerIconPath } from '@/features/book/lib/provider-icons'
+import { lubimyczytacBookUrl } from '@/features/book/lib/provider-links'
import { useBookDetail } from '../composables/useBookDetail'
import { useCoverVersions } from '../composables/useCoverVersions'
import { getFormatColor } from '../lib/format-colors'
@@ -144,6 +145,15 @@ const providerLinks = computed
(() => {
fallback: 'RN',
})
}
+ if (ids.lubimyczytac) {
+ out.push({
+ key: 'lubimyczytac',
+ label: 'LubimyCzytac',
+ url: lubimyczytacBookUrl(ids.lubimyczytac),
+ iconUrl: providerIconPath('lubimyczytac'),
+ fallback: 'LC',
+ })
+ }
return out
})
diff --git a/client/src/features/book/components/detail/tabs/DetailsTab.vue b/client/src/features/book/components/detail/tabs/DetailsTab.vue
index 433d47fc8..176d6733b 100644
--- a/client/src/features/book/components/detail/tabs/DetailsTab.vue
+++ b/client/src/features/book/components/detail/tabs/DetailsTab.vue
@@ -21,6 +21,7 @@ import {
import { DialogClose, DialogContent, DialogOverlay, DialogPortal, DialogRoot } from 'reka-ui'
import { getFormatColor } from '@/features/book/lib/format-colors'
import { providerIconPath } from '@/features/book/lib/provider-icons'
+import { lubimyczytacBookUrl } from '@/features/book/lib/provider-links'
import { getProviderColor } from '@/lib/provider-colors'
import { useCoverVersions } from '@/features/book/composables/useCoverVersions'
import { COVER_ASPECT_RATIO_KEY, DEFAULT_COVER_ASPECT_RATIO } from '@/features/book/lib/cover-aspect-ratio'
@@ -601,6 +602,15 @@ const providerLinks = computed(() => {
fallback: 'RN',
})
}
+ if (ids.lubimyczytac) {
+ out.push({
+ key: 'lubimyczytac',
+ label: 'LubimyCzytac',
+ url: lubimyczytacBookUrl(ids.lubimyczytac),
+ iconUrl: providerIconPath('lubimyczytac'),
+ fallback: 'LC',
+ })
+ }
return out
})
diff --git a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
index e40066aa5..c4accd8ab 100644
--- a/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
+++ b/client/src/features/book/components/detail/tabs/EditMetadataTab.vue
@@ -73,6 +73,7 @@ const DIRECT_PATCH_FIELDS = [
'koboId',
'comicvineId',
'ranobedbId',
+ 'lubimyczytacId',
] as const
const COMIC_FIELD_MAP = {
@@ -169,6 +170,7 @@ const providerIdFields = [
{ field: 'koboId' as const, label: 'Kobo' },
{ field: 'comicvineId' as const, label: 'ComicVine' },
{ field: 'ranobedbId' as const, label: 'RanobeDB' },
+ { field: 'lubimyczytacId' as const, label: 'LubimyCzytac' },
]
function setIntField(field: 'publishedYear' | 'pageCount' | 'durationSeconds', e: Event) {
@@ -421,6 +423,7 @@ function buildPreviewPatch(preview: MetadataRefreshPreview): MetadataPatch {
koboId: preview.koboId,
comicvineId: preview.comicvineId,
ranobedbId: preview.ranobedbId,
+ lubimyczytacId: preview.lubimyczytacId,
comicMetadata: preview.comicMetadata,
narrators: preview.audioMetadata?.narrators,
durationSeconds: preview.audioMetadata?.durationSeconds ?? undefined,
diff --git a/client/src/features/book/composables/useFileMetadata.ts b/client/src/features/book/composables/useFileMetadata.ts
index f3a9c4ed8..7965becda 100644
--- a/client/src/features/book/composables/useFileMetadata.ts
+++ b/client/src/features/book/composables/useFileMetadata.ts
@@ -24,6 +24,7 @@ export interface FileMetadata {
koboId?: string | null
comicvineId?: string | null
ranobedbId?: string | null
+ lubimyczytacId?: string | null
authors?: string[]
genres?: string[]
narrators?: string[]
diff --git a/client/src/features/book/composables/useMetadataDiff.ts b/client/src/features/book/composables/useMetadataDiff.ts
index 504e3bdb1..6bfef889e 100644
--- a/client/src/features/book/composables/useMetadataDiff.ts
+++ b/client/src/features/book/composables/useMetadataDiff.ts
@@ -96,6 +96,7 @@ export interface MetadataPatch {
koboId?: string | null
comicvineId?: string | null
ranobedbId?: string | null
+ lubimyczytacId?: string | null
comicMetadata?: ComicMetadataFields
}
@@ -158,6 +159,7 @@ export type ProviderIdPatchField =
| 'koboId'
| 'comicvineId'
| 'ranobedbId'
+ | 'lubimyczytacId'
export const PROVIDER_ID_FIELD: Record = {
google: 'googleBooksId',
@@ -171,6 +173,7 @@ export const PROVIDER_ID_FIELD: Record(Object.values(PROVIDER_ID_FIELD).filter((v): v is ProviderIdPatchField => v !== undefined))
@@ -191,6 +194,7 @@ export const PROVIDER_ID_LABEL: Record = {
comicvine: 'ComicVine ID',
ranobedb: 'RanobeDB ID',
kobo: 'Kobo ID',
+ lubimyczytac: 'LubimyCzytac ID',
}
export function getCandidateValueFrom(candidate: MetadataCandidate, key: DiffFieldKey): string {
diff --git a/client/src/features/book/composables/useMetadataEditor.ts b/client/src/features/book/composables/useMetadataEditor.ts
index c8868572e..3449d2b5b 100644
--- a/client/src/features/book/composables/useMetadataEditor.ts
+++ b/client/src/features/book/composables/useMetadataEditor.ts
@@ -28,6 +28,7 @@ const ROOT_FIELDS = [
'koboId',
'comicvineId',
'ranobedbId',
+ 'lubimyczytacId',
] as const
const COMIC_FIELDS = {
@@ -83,6 +84,7 @@ export function useMetadataEditor() {
koboId: null as string | null,
comicvineId: null as string | null,
ranobedbId: null as string | null,
+ lubimyczytacId: null as string | null,
comicIssueNumber: null as string | null,
comicVolumeName: null as string | null,
comicStoryArcs: [] as string[],
@@ -130,6 +132,7 @@ export function useMetadataEditor() {
form.koboId = book.providerIds.kobo ?? null
form.comicvineId = book.providerIds.comicvine ?? null
form.ranobedbId = book.providerIds.ranobedb ?? null
+ form.lubimyczytacId = book.providerIds.lubimyczytac ?? null
const cm = book.comicMetadata
form.comicIssueNumber = cm?.issueNumber ?? null
form.comicVolumeName = cm?.volumeName ?? null
diff --git a/client/src/features/book/lib/file-metadata-patch.ts b/client/src/features/book/lib/file-metadata-patch.ts
index 13a016511..c1e22e1c2 100644
--- a/client/src/features/book/lib/file-metadata-patch.ts
+++ b/client/src/features/book/lib/file-metadata-patch.ts
@@ -23,6 +23,7 @@ const FILE_METADATA_PATCH_FIELDS = [
'koboId',
'comicvineId',
'ranobedbId',
+ 'lubimyczytacId',
'authors',
'genres',
'narrators',
diff --git a/client/src/features/book/lib/metadata-refresh-feedback.ts b/client/src/features/book/lib/metadata-refresh-feedback.ts
index b5e5a88af..a6fba64a5 100644
--- a/client/src/features/book/lib/metadata-refresh-feedback.ts
+++ b/client/src/features/book/lib/metadata-refresh-feedback.ts
@@ -17,6 +17,7 @@ const PROVIDER_LABELS: Record = {
[MetadataProviderKey.COMICVINE]: 'ComicVine',
[MetadataProviderKey.RANOBEDB]: 'RanobeDB',
[MetadataProviderKey.KOBO]: 'Kobo',
+ [MetadataProviderKey.LUBIMYCZYTAC]: 'LubimyCzytac',
}
export function metadataRefreshEmptyMessage(diagnostics: MetadataFetchDiagnostics, book: BookDetail): string {
diff --git a/client/src/features/book/lib/provider-icons.ts b/client/src/features/book/lib/provider-icons.ts
index 727ce900c..a26577ebc 100644
--- a/client/src/features/book/lib/provider-icons.ts
+++ b/client/src/features/book/lib/provider-icons.ts
@@ -10,6 +10,7 @@ const PROVIDER_ICON_PATHS = {
audible: `${PROVIDER_ICON_BASE_PATH}/audible.svg`,
kobo: `${PROVIDER_ICON_BASE_PATH}/kobo.svg`,
ranobedb: `${PROVIDER_ICON_BASE_PATH}/ranobedb.svg`,
+ lubimyczytac: `${PROVIDER_ICON_BASE_PATH}/lubimyczytac.svg`,
} as const
type ProviderIconKey = keyof typeof PROVIDER_ICON_PATHS
diff --git a/client/src/features/book/lib/provider-links.ts b/client/src/features/book/lib/provider-links.ts
new file mode 100644
index 000000000..c613fd5ff
--- /dev/null
+++ b/client/src/features/book/lib/provider-links.ts
@@ -0,0 +1,7 @@
+export function lubimyczytacBookUrl(id: string): string {
+ const value = String(id).trim()
+ // New data stores the canonical "id/slug" path. lubimyczytac.pl 404s on a slug-less id,
+ // so legacy bare-numeric ids get a placeholder slug segment to keep the link valid.
+ const path = value.includes('/') ? value : `${value}/-`
+ return `https://lubimyczytac.pl/ksiazka/${path.split('/').map(encodeURIComponent).join('/')}`
+}
diff --git a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
index 6daabfc43..163d32ceb 100644
--- a/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
+++ b/client/src/features/settings/metadata-preferences/components/ProviderConfigPanel.vue
@@ -198,6 +198,12 @@ const rows: RowDef[] = [
},
},
{ key: 'ranobedb', label: 'RanobeDB', hint: 'Japanese light novel metadata from RanobeDB. No setup required.', fields: [] },
+ {
+ key: 'lubimyczytac',
+ label: 'LubimyCzytac',
+ hint: 'Polish book catalog (lubimyczytac.pl). Scrapes public book pages. No setup required.',
+ fields: [],
+ },
]
const TESTABLE_PROVIDERS: MetadataProviderKey[] = [MetadataProviderKey.AMAZON, MetadataProviderKey.HARDCOVER]
diff --git a/client/src/lib/provider-colors.ts b/client/src/lib/provider-colors.ts
index 3f1976591..d63316417 100644
--- a/client/src/lib/provider-colors.ts
+++ b/client/src/lib/provider-colors.ts
@@ -12,6 +12,7 @@ const PROVIDER_HEX: Record = {
comicvine: '#ffdb0f',
ranobedb: '#a78cff',
kobo: '#e23434',
+ lubimyczytac: '#f47373',
auto: '#8B5CF6',
}
@@ -29,6 +30,7 @@ export const PROVIDER_SHORT_LABELS: Record = {
comicvine: 'ComicVine',
ranobedb: 'RanobeDB',
kobo: 'Kobo',
+ lubimyczytac: 'LubimyCzytac',
auto: 'Fetched',
}
diff --git a/packages/types/src/book.ts b/packages/types/src/book.ts
index 6ce573f58..ac656cc6e 100644
--- a/packages/types/src/book.ts
+++ b/packages/types/src/book.ts
@@ -161,6 +161,7 @@ export type BookMetadataRefreshPreviewFields = {
koboId?: string | null;
comicvineId?: string | null;
ranobedbId?: string | null;
+ lubimyczytacId?: string | null;
audioMetadata?: {
narrators?: string[];
durationSeconds?: number | null;
diff --git a/packages/types/src/file-write.ts b/packages/types/src/file-write.ts
index 6c2d3fc46..df26895f9 100644
--- a/packages/types/src/file-write.ts
+++ b/packages/types/src/file-write.ts
@@ -24,6 +24,7 @@ export const COMMON_PROVIDER_BOOK_FILE_WRITE_FIELDS = [
"openLibraryId",
"ranobedbId",
"koboId",
+ "lubimyczytacId",
] as const;
export const COMIC_BOOK_FILE_WRITE_FIELDS = [
@@ -75,6 +76,7 @@ export const BOOK_FILE_WRITE_FIELD_LABELS = {
openLibraryId: "Open Library ID",
ranobedbId: "RanobeDB ID",
koboId: "Kobo ID",
+ lubimyczytacId: "LubimyCzytac ID",
comicIssueNumber: "Issue number",
comicVolumeName: "Volume",
comicPencillers: "Pencillers",
diff --git a/packages/types/src/metadata-fetch.ts b/packages/types/src/metadata-fetch.ts
index fec549f70..35c1386a2 100644
--- a/packages/types/src/metadata-fetch.ts
+++ b/packages/types/src/metadata-fetch.ts
@@ -12,6 +12,7 @@ export const MetadataProviderKey = {
COMICVINE: "comicvine",
RANOBEDB: "ranobedb",
KOBO: "kobo",
+ LUBIMYCZYTAC: "lubimyczytac",
} as const;
export interface ComicMetadataFields {
diff --git a/packages/types/src/metadata-lock.ts b/packages/types/src/metadata-lock.ts
index 201908e87..5b421ae54 100644
--- a/packages/types/src/metadata-lock.ts
+++ b/packages/types/src/metadata-lock.ts
@@ -27,6 +27,7 @@ export const BOOK_METADATA_LOCK_FIELDS = [
"koboId",
"comicvineId",
"ranobedbId",
+ "lubimyczytacId",
"comicIssueNumber",
"comicVolumeName",
"comicStoryArcs",
@@ -75,7 +76,19 @@ export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [
{
key: "providers",
label: "Provider IDs",
- fields: ["googleBooksId", "goodreadsId", "amazonId", "hardcoverId", "openLibraryId", "itunesId", "audibleId", "koboId", "comicvineId", "ranobedbId"],
+ fields: [
+ "googleBooksId",
+ "goodreadsId",
+ "amazonId",
+ "hardcoverId",
+ "openLibraryId",
+ "itunesId",
+ "audibleId",
+ "koboId",
+ "comicvineId",
+ "ranobedbId",
+ "lubimyczytacId",
+ ],
},
{
key: "comic",
diff --git a/packages/types/src/metadata-preferences.ts b/packages/types/src/metadata-preferences.ts
index 3be278ae2..2181010f9 100644
--- a/packages/types/src/metadata-preferences.ts
+++ b/packages/types/src/metadata-preferences.ts
@@ -127,6 +127,7 @@ export interface ProviderConfigurations {
comicvine: ComicVineProviderConfig;
ranobedb: SimpleProviderConfig;
kobo: KoboProviderConfig;
+ lubimyczytac: SimpleProviderConfig;
}
export interface ProviderStatus {
diff --git a/server/src/db/migrations/0019_add_lubimyczytac_provider_id.sql b/server/src/db/migrations/0019_add_lubimyczytac_provider_id.sql
new file mode 100644
index 000000000..98806cfa6
--- /dev/null
+++ b/server/src/db/migrations/0019_add_lubimyczytac_provider_id.sql
@@ -0,0 +1 @@
+ALTER TABLE "book_metadata" ADD COLUMN "lubimyczytac_id" text;
\ No newline at end of file
diff --git a/server/src/db/migrations/meta/0019_snapshot.json b/server/src/db/migrations/meta/0019_snapshot.json
new file mode 100644
index 000000000..9f02164de
--- /dev/null
+++ b/server/src/db/migrations/meta/0019_snapshot.json
@@ -0,0 +1,10767 @@
+{
+ "id": "e207fce1-b594-4b94-8319-60b114ea8ddf",
+ "prevId": "834a29e8-b029-4ecc-ac94-a247b506168e",
+ "version": "7",
+ "dialect": "postgresql",
+ "tables": {
+ "public.audit_log": {
+ "name": "audit_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "bigserial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "actor_username": {
+ "name": "actor_username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "action": {
+ "name": "action",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "resource": {
+ "name": "resource",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "resource_id": {
+ "name": "resource_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ip": {
+ "name": "ip",
+ "type": "varchar(45)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "idx_audit_user_id": {
+ "name": "idx_audit_user_id",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_resource": {
+ "name": "idx_audit_resource",
+ "columns": [
+ {
+ "expression": "resource",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "resource_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_action": {
+ "name": "idx_audit_action",
+ "columns": [
+ {
+ "expression": "action",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_ip": {
+ "name": "idx_audit_ip",
+ "columns": [
+ {
+ "expression": "ip",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "idx_audit_created_at": {
+ "name": "idx_audit_created_at",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audit_log_user_id_users_id_fk": {
+ "name": "audit_log_user_id_users_id_fk",
+ "tableFrom": "audit_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.app_settings": {
+ "name": "app_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value": {
+ "name": "value",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "app_settings_key_unique": {
+ "name": "app_settings_key_unique",
+ "nullsNotDistinct": false,
+ "columns": ["key"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.magic_access_tokens": {
+ "name": "magic_access_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by": {
+ "name": "created_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "label": {
+ "name": "label",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "raw_token": {
+ "name": "raw_token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_active": {
+ "name": "is_active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_used_at": {
+ "name": "last_used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "use_count": {
+ "name": "use_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "magic_access_tokens_user_id_idx": {
+ "name": "magic_access_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "magic_access_tokens_user_id_users_id_fk": {
+ "name": "magic_access_tokens_user_id_users_id_fk",
+ "tableFrom": "magic_access_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "magic_access_tokens_created_by_users_id_fk": {
+ "name": "magic_access_tokens_created_by_users_id_fk",
+ "tableFrom": "magic_access_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["created_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "magic_access_tokens_token_hash_unique": {
+ "name": "magic_access_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "magic_access_tokens_use_count_nonneg_chk": {
+ "name": "magic_access_tokens_use_count_nonneg_chk",
+ "value": "\"magic_access_tokens\".\"use_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.password_reset_tokens": {
+ "name": "password_reset_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "used_at": {
+ "name": "used_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "password_reset_tokens_user_id_idx": {
+ "name": "password_reset_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "password_reset_tokens_expires_at_idx": {
+ "name": "password_reset_tokens_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "password_reset_tokens_user_id_users_id_fk": {
+ "name": "password_reset_tokens_user_id_users_id_fk",
+ "tableFrom": "password_reset_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "password_reset_tokens_token_hash_unique": {
+ "name": "password_reset_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "password_reset_tokens_expires_after_created_chk": {
+ "name": "password_reset_tokens_expires_after_created_chk",
+ "value": "\"password_reset_tokens\".\"expires_at\" > \"password_reset_tokens\".\"created_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.refresh_tokens": {
+ "name": "refresh_tokens",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token_hash": {
+ "name": "token_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "revoked_at": {
+ "name": "revoked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rotated_at": {
+ "name": "rotated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "replaced_by_token_hash": {
+ "name": "replaced_by_token_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "refresh_tokens_user_id_idx": {
+ "name": "refresh_tokens_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "refresh_tokens_expires_at_idx": {
+ "name": "refresh_tokens_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "refresh_tokens_user_id_users_id_fk": {
+ "name": "refresh_tokens_user_id_users_id_fk",
+ "tableFrom": "refresh_tokens",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "refresh_tokens_token_hash_unique": {
+ "name": "refresh_tokens_token_hash_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token_hash"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "refresh_tokens_expires_after_created_chk": {
+ "name": "refresh_tokens_expires_after_created_chk",
+ "value": "\"refresh_tokens\".\"expires_at\" > \"refresh_tokens\".\"created_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_content_filter_genres": {
+ "name": "user_content_filter_genres",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filter_type": {
+ "name": "filter_type",
+ "type": "content_filter_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "genre_id": {
+ "name": "genre_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_content_filter_genres_genre_id_idx": {
+ "name": "user_content_filter_genres_genre_id_idx",
+ "columns": [
+ {
+ "expression": "genre_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_content_filter_genres_user_id_users_id_fk": {
+ "name": "user_content_filter_genres_user_id_users_id_fk",
+ "tableFrom": "user_content_filter_genres",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_content_filter_genres_genre_id_genres_id_fk": {
+ "name": "user_content_filter_genres_genre_id_genres_id_fk",
+ "tableFrom": "user_content_filter_genres",
+ "tableTo": "genres",
+ "columnsFrom": ["genre_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_content_filter_genres_user_id_filter_type_genre_id_pk": {
+ "name": "user_content_filter_genres_user_id_filter_type_genre_id_pk",
+ "columns": ["user_id", "filter_type", "genre_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_content_filter_tags": {
+ "name": "user_content_filter_tags",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "filter_type": {
+ "name": "filter_type",
+ "type": "content_filter_type",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "user_content_filter_tags_tag_id_idx": {
+ "name": "user_content_filter_tags_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_content_filter_tags_user_id_users_id_fk": {
+ "name": "user_content_filter_tags_user_id_users_id_fk",
+ "tableFrom": "user_content_filter_tags",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_content_filter_tags_tag_id_tags_id_fk": {
+ "name": "user_content_filter_tags_tag_id_tags_id_fk",
+ "tableFrom": "user_content_filter_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_content_filter_tags_user_id_filter_type_tag_id_pk": {
+ "name": "user_content_filter_tags_user_id_filter_type_tag_id_pk",
+ "columns": ["user_id", "filter_type", "tag_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_library_access": {
+ "name": "user_library_access",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "access_level": {
+ "name": "access_level",
+ "type": "library_access_level",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'viewer'"
+ }
+ },
+ "indexes": {
+ "user_library_access_library_user_idx": {
+ "name": "user_library_access_library_user_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_library_access_user_id_users_id_fk": {
+ "name": "user_library_access_user_id_users_id_fk",
+ "tableFrom": "user_library_access",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_library_access_library_id_libraries_id_fk": {
+ "name": "user_library_access_library_id_libraries_id_fk",
+ "tableFrom": "user_library_access",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_library_access_user_id_library_id_pk": {
+ "name": "user_library_access_user_id_library_id_pk",
+ "columns": ["user_id", "library_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_permissions": {
+ "name": "user_permissions",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "user_permissions_user_id_users_id_fk": {
+ "name": "user_permissions_user_id_users_id_fk",
+ "tableFrom": "user_permissions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_permissions_user_id_permission_name_pk": {
+ "name": "user_permissions_user_id_permission_name_pk",
+ "columns": ["user_id", "permission_name"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.users": {
+ "name": "users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "active": {
+ "name": "active",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_superuser": {
+ "name": "is_superuser",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_default_password": {
+ "name": "is_default_password",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "token_version": {
+ "name": "token_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "failed_login_attempts": {
+ "name": "failed_login_attempts",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "locked_until": {
+ "name": "locked_until",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_url": {
+ "name": "avatar_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "avatar_source": {
+ "name": "avatar_source",
+ "type": "user_avatar_source",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'none'"
+ },
+ "avatar_version": {
+ "name": "avatar_version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "provisioning_method": {
+ "name": "provisioning_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'local'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "users_username_lower_uidx": {
+ "name": "users_username_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_email_lower_uidx": {
+ "name": "users_email_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"users\".\"email\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "users_oidc_subject_issuer_uidx": {
+ "name": "users_oidc_subject_issuer_uidx",
+ "columns": [
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"users\".\"oidc_subject\" is not null and \"users\".\"oidc_issuer\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "users_username_unique": {
+ "name": "users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": ["username"]
+ },
+ "users_email_unique": {
+ "name": "users_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["email"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "users_provisioning_method_chk": {
+ "name": "users_provisioning_method_chk",
+ "value": "\"users\".\"provisioning_method\" in ('local', 'manual', 'oidc', 'shared')"
+ },
+ "users_token_version_nonnegative_chk": {
+ "name": "users_token_version_nonnegative_chk",
+ "value": "\"users\".\"token_version\" >= 0"
+ },
+ "users_failed_login_attempts_nonnegative_chk": {
+ "name": "users_failed_login_attempts_nonnegative_chk",
+ "value": "\"users\".\"failed_login_attempts\" >= 0"
+ },
+ "users_avatar_version_nonnegative_chk": {
+ "name": "users_avatar_version_nonnegative_chk",
+ "value": "\"users\".\"avatar_version\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.comic_metadata": {
+ "name": "comic_metadata",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "issue_number": {
+ "name": "issue_number",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "volume_name": {
+ "name": "volume_name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "pencillers": {
+ "name": "pencillers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "inkers": {
+ "name": "inkers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "colorists": {
+ "name": "colorists",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "letterers": {
+ "name": "letterers",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_artists": {
+ "name": "cover_artists",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "characters": {
+ "name": "characters",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "teams": {
+ "name": "teams",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locations": {
+ "name": "locations",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "story_arcs": {
+ "name": "story_arcs",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "comic_metadata_book_id_books_id_fk": {
+ "name": "comic_metadata_book_id_books_id_fk",
+ "tableFrom": "comic_metadata",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "comic_metadata_book_id_unique": {
+ "name": "comic_metadata_book_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.migration_plan_artifacts": {
+ "name": "migration_plan_artifacts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_snapshot_hash": {
+ "name": "source_snapshot_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_hash": {
+ "name": "profile_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_hash": {
+ "name": "plan_hash",
+ "type": "varchar(128)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan": {
+ "name": "plan",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "source_data": {
+ "name": "source_data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "summary": {
+ "name": "summary",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_plan_artifacts_plan_hash_uidx": {
+ "name": "migration_plan_artifacts_plan_hash_uidx",
+ "columns": [
+ {
+ "expression": "plan_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_plan_artifacts_source_id_idx": {
+ "name": "migration_plan_artifacts_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_plan_artifacts_profile_id_idx": {
+ "name": "migration_plan_artifacts_profile_id_idx",
+ "columns": [
+ {
+ "expression": "profile_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_plan_artifacts_source_id_migration_sources_id_fk": {
+ "name": "migration_plan_artifacts_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_profile_id_migration_profiles_id_fk": {
+ "name": "migration_plan_artifacts_profile_id_migration_profiles_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_created_by_user_id_users_id_fk": {
+ "name": "migration_plan_artifacts_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_plan_artifacts_profile_source_fk": {
+ "name": "migration_plan_artifacts_profile_source_fk",
+ "tableFrom": "migration_plan_artifacts",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id", "source_id"],
+ "columnsTo": ["id", "source_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "migration_plan_artifacts_id_source_profile_unique": {
+ "name": "migration_plan_artifacts_id_source_profile_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "source_id", "profile_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.migration_profiles": {
+ "name": "migration_profiles",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "version": {
+ "name": "version",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "user_mappings": {
+ "name": "user_mappings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path_mappings": {
+ "name": "path_mappings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "scope": {
+ "name": "scope",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::jsonb"
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_profiles_source_name_version_uidx": {
+ "name": "migration_profiles_source_name_version_uidx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "version",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_profiles_source_id_idx": {
+ "name": "migration_profiles_source_id_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_profiles_source_id_migration_sources_id_fk": {
+ "name": "migration_profiles_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_profiles",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_profiles_created_by_user_id_users_id_fk": {
+ "name": "migration_profiles_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_profiles",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "migration_profiles_id_source_id_unique": {
+ "name": "migration_profiles_id_source_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "source_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "migration_profiles_version_positive_chk": {
+ "name": "migration_profiles_version_positive_chk",
+ "value": "\"migration_profiles\".\"version\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_run_metrics": {
+ "name": "migration_run_metrics",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "run_id": {
+ "name": "run_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stage": {
+ "name": "stage",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "processed": {
+ "name": "processed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "imported": {
+ "name": "imported",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "skipped": {
+ "name": "skipped",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "unresolved": {
+ "name": "unresolved",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "failed": {
+ "name": "failed",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_run_metrics_run_stage_entity_uidx": {
+ "name": "migration_run_metrics_run_stage_entity_uidx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "stage",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_run_metrics_run_id_idx": {
+ "name": "migration_run_metrics_run_id_idx",
+ "columns": [
+ {
+ "expression": "run_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_run_metrics_run_id_migration_runs_id_fk": {
+ "name": "migration_run_metrics_run_id_migration_runs_id_fk",
+ "tableFrom": "migration_run_metrics",
+ "tableTo": "migration_runs",
+ "columnsFrom": ["run_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "migration_run_metrics_processed_nonnegative_chk": {
+ "name": "migration_run_metrics_processed_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"processed\" >= 0"
+ },
+ "migration_run_metrics_imported_nonnegative_chk": {
+ "name": "migration_run_metrics_imported_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"imported\" >= 0"
+ },
+ "migration_run_metrics_skipped_nonnegative_chk": {
+ "name": "migration_run_metrics_skipped_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"skipped\" >= 0"
+ },
+ "migration_run_metrics_unresolved_nonnegative_chk": {
+ "name": "migration_run_metrics_unresolved_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"unresolved\" >= 0"
+ },
+ "migration_run_metrics_failed_nonnegative_chk": {
+ "name": "migration_run_metrics_failed_nonnegative_chk",
+ "value": "\"migration_run_metrics\".\"failed\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_runs": {
+ "name": "migration_runs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "source_id": {
+ "name": "source_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "profile_id": {
+ "name": "profile_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "plan_artifact_id": {
+ "name": "plan_artifact_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_key": {
+ "name": "target_key",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'bookorbit'"
+ },
+ "state": {
+ "name": "state",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'draft'"
+ },
+ "current_stage": {
+ "name": "current_stage",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "triggered_by_user_id": {
+ "name": "triggered_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_runs_source_target_state_idx": {
+ "name": "migration_runs_source_target_state_idx",
+ "columns": [
+ {
+ "expression": "source_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "target_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_runs_state_idx": {
+ "name": "migration_runs_state_idx",
+ "columns": [
+ {
+ "expression": "state",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_runs_source_id_migration_sources_id_fk": {
+ "name": "migration_runs_source_id_migration_sources_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_sources",
+ "columnsFrom": ["source_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_runs_profile_id_migration_profiles_id_fk": {
+ "name": "migration_runs_profile_id_migration_profiles_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "migration_runs_plan_artifact_id_migration_plan_artifacts_id_fk": {
+ "name": "migration_runs_plan_artifact_id_migration_plan_artifacts_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_plan_artifacts",
+ "columnsFrom": ["plan_artifact_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_runs_triggered_by_user_id_users_id_fk": {
+ "name": "migration_runs_triggered_by_user_id_users_id_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "users",
+ "columnsFrom": ["triggered_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "migration_runs_profile_source_fk": {
+ "name": "migration_runs_profile_source_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_profiles",
+ "columnsFrom": ["profile_id", "source_id"],
+ "columnsTo": ["id", "source_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "migration_runs_plan_artifact_source_profile_fk": {
+ "name": "migration_runs_plan_artifact_source_profile_fk",
+ "tableFrom": "migration_runs",
+ "tableTo": "migration_plan_artifacts",
+ "columnsFrom": ["plan_artifact_id", "source_id", "profile_id"],
+ "columnsTo": ["id", "source_id", "profile_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "migration_runs_state_chk": {
+ "name": "migration_runs_state_chk",
+ "value": "\"state\" IN ('draft', 'preflight_failed', 'dry_run_ready', 'running', 'failed', 'completed')"
+ },
+ "migration_runs_started_before_ended_chk": {
+ "name": "migration_runs_started_before_ended_chk",
+ "value": "\"migration_runs\".\"ended_at\" is null or \"migration_runs\".\"started_at\" is null or \"migration_runs\".\"ended_at\" >= \"migration_runs\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.migration_sources": {
+ "name": "migration_sources",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "connection_config": {
+ "name": "connection_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "capabilities": {
+ "name": "capabilities",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_validated_at": {
+ "name": "last_validated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_by_user_id": {
+ "name": "created_by_user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "migration_sources_type_name_uidx": {
+ "name": "migration_sources_type_name_uidx",
+ "columns": [
+ {
+ "expression": "type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "migration_sources_created_by_user_id_idx": {
+ "name": "migration_sources_created_by_user_id_idx",
+ "columns": [
+ {
+ "expression": "created_by_user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "migration_sources_created_by_user_id_users_id_fk": {
+ "name": "migration_sources_created_by_user_id_users_id_fk",
+ "tableFrom": "migration_sources",
+ "tableTo": "users",
+ "columnsFrom": ["created_by_user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.libraries": {
+ "name": "libraries",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cover_aspect_ratio": {
+ "name": "cover_aspect_ratio",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'2/3'"
+ },
+ "watch": {
+ "name": "watch",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "auto_scan_cron_expression": {
+ "name": "auto_scan_cron_expression",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_precedence": {
+ "name": "metadata_precedence",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"folderStructure\",\"embedded\",\"nfoFile\",\"opfFile\",\"sidecar\"]'::jsonb"
+ },
+ "format_priority": {
+ "name": "format_priority",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[\"epub\",\"pdf\",\"cbz\",\"cbr\",\"cb7\",\"mobi\",\"azw3\",\"azw\",\"fb2\",\"m4b\",\"mp3\",\"m4a\",\"opus\",\"ogg\",\"flac\"]'::jsonb"
+ },
+ "allowed_formats": {
+ "name": "allowed_formats",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "organization_mode": {
+ "name": "organization_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'book_per_folder'"
+ },
+ "exclude_patterns": {
+ "name": "exclude_patterns",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "reading_threshold": {
+ "name": "reading_threshold",
+ "type": "double precision",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0.25
+ },
+ "mark_as_finished_percent_complete": {
+ "name": "mark_as_finished_percent_complete",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 98
+ },
+ "file_write_enabled": {
+ "name": "file_write_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_write_cover": {
+ "name": "file_write_write_cover",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_epub_enabled": {
+ "name": "file_write_epub_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_epub_max_file_size_mb": {
+ "name": "file_write_epub_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_pdf_enabled": {
+ "name": "file_write_pdf_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "file_write_pdf_max_file_size_mb": {
+ "name": "file_write_pdf_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "file_write_cbx_enabled": {
+ "name": "file_write_cbx_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_cbx_max_file_size_mb": {
+ "name": "file_write_cbx_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "file_write_audio_enabled": {
+ "name": "file_write_audio_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_write_audio_max_file_size_mb": {
+ "name": "file_write_audio_max_file_size_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 500
+ },
+ "file_rename_enabled": {
+ "name": "file_rename_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_naming_pattern": {
+ "name": "file_naming_pattern",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_fetch_preferences": {
+ "name": "metadata_fetch_preferences",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_config": {
+ "name": "book_metadata_fetch_config",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_run_at": {
+ "name": "book_metadata_fetch_last_run_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_metadata_fetch_last_queued_count": {
+ "name": "book_metadata_fetch_last_queued_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scan_mode": {
+ "name": "scan_mode",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "poll_interval_seconds": {
+ "name": "poll_interval_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "default": 300
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "libraries_name_lower_uidx": {
+ "name": "libraries_name_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "libraries_display_order_nonnegative_chk": {
+ "name": "libraries_display_order_nonnegative_chk",
+ "value": "\"libraries\".\"display_order\" >= 0"
+ },
+ "libraries_organization_mode_chk": {
+ "name": "libraries_organization_mode_chk",
+ "value": "\"libraries\".\"organization_mode\" in ('book_per_folder', 'book_per_file')"
+ },
+ "libraries_reading_threshold_range_chk": {
+ "name": "libraries_reading_threshold_range_chk",
+ "value": "\"libraries\".\"reading_threshold\" >= 0 and \"libraries\".\"reading_threshold\" <= 100"
+ },
+ "libraries_mark_finished_percent_range_chk": {
+ "name": "libraries_mark_finished_percent_range_chk",
+ "value": "\"libraries\".\"mark_as_finished_percent_complete\" >= 0 and \"libraries\".\"mark_as_finished_percent_complete\" <= 100"
+ },
+ "libraries_scan_mode_chk": {
+ "name": "libraries_scan_mode_chk",
+ "value": "\"libraries\".\"scan_mode\" in ('auto', 'manual')"
+ },
+ "libraries_poll_interval_nonnegative_chk": {
+ "name": "libraries_poll_interval_nonnegative_chk",
+ "value": "\"libraries\".\"poll_interval_seconds\" is null or \"libraries\".\"poll_interval_seconds\" >= 0"
+ },
+ "libraries_file_write_epub_max_size_chk": {
+ "name": "libraries_file_write_epub_max_size_chk",
+ "value": "\"libraries\".\"file_write_epub_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_pdf_max_size_chk": {
+ "name": "libraries_file_write_pdf_max_size_chk",
+ "value": "\"libraries\".\"file_write_pdf_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_cbx_max_size_chk": {
+ "name": "libraries_file_write_cbx_max_size_chk",
+ "value": "\"libraries\".\"file_write_cbx_max_file_size_mb\" >= 1"
+ },
+ "libraries_file_write_audio_max_size_chk": {
+ "name": "libraries_file_write_audio_max_size_chk",
+ "value": "\"libraries\".\"file_write_audio_max_file_size_mb\" >= 1"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.library_folders": {
+ "name": "library_folders",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "path": {
+ "name": "path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "library_folders_library_id_idx": {
+ "name": "library_folders_library_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "library_folders_library_path_uidx": {
+ "name": "library_folders_library_path_uidx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "library_folders_library_id_libraries_id_fk": {
+ "name": "library_folders_library_id_libraries_id_fk",
+ "tableFrom": "library_folders",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "library_folders_id_library_id_unique": {
+ "name": "library_folders_id_library_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "library_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_files": {
+ "name": "book_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absolute_path": {
+ "name": "absolute_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rel_path": {
+ "name": "rel_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ino": {
+ "name": "ino",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "size_bytes": {
+ "name": "size_bytes",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "mtime": {
+ "name": "mtime",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'content'"
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_files_absolute_path_uidx": {
+ "name": "book_files_absolute_path_uidx",
+ "columns": [
+ {
+ "expression": "absolute_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_book_id_idx": {
+ "name": "book_files_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_id_idx": {
+ "name": "book_files_library_folder_id_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_file_hash_idx": {
+ "name": "book_files_file_hash_idx",
+ "columns": [
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_ino_idx": {
+ "name": "book_files_ino_idx",
+ "columns": [
+ {
+ "expression": "ino",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_format_idx": {
+ "name": "book_files_format_idx",
+ "columns": [
+ {
+ "expression": "format",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_file_hash_idx": {
+ "name": "book_files_library_folder_file_hash_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_files_library_folder_ino_idx": {
+ "name": "book_files_library_folder_ino_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "ino",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_files_book_id_books_id_fk": {
+ "name": "book_files_book_id_books_id_fk",
+ "tableFrom": "book_files",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_files_library_folder_id_library_folders_id_fk": {
+ "name": "book_files_library_folder_id_library_folders_id_fk",
+ "tableFrom": "book_files",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_files_book_folder_consistency_fk": {
+ "name": "book_files_book_folder_consistency_fk",
+ "tableFrom": "book_files",
+ "tableTo": "books",
+ "columnsFrom": ["book_id", "library_folder_id"],
+ "columnsTo": ["id", "library_folder_id"],
+ "onDelete": "no action",
+ "onUpdate": "cascade"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_files_role_chk": {
+ "name": "book_files_role_chk",
+ "value": "\"book_files\".\"role\" in ('content', 'cover', 'metadata', 'supplement')"
+ },
+ "book_files_size_bytes_nonnegative_chk": {
+ "name": "book_files_size_bytes_nonnegative_chk",
+ "value": "\"book_files\".\"size_bytes\" is null or \"book_files\".\"size_bytes\" >= 0"
+ },
+ "book_files_duration_seconds_nonnegative_chk": {
+ "name": "book_files_duration_seconds_nonnegative_chk",
+ "value": "\"book_files\".\"duration_seconds\" is null or \"book_files\".\"duration_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.books": {
+ "name": "books",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "primary_file_id": {
+ "name": "primary_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "primary_author_sort_name": {
+ "name": "primary_author_sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "folder_path": {
+ "name": "folder_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'present'"
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "books_library_id_folder_path_idx": {
+ "name": "books_library_id_folder_path_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "folder_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_primary_file_id_idx": {
+ "name": "books_primary_file_id_idx",
+ "columns": [
+ {
+ "expression": "primary_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_status_idx": {
+ "name": "books_library_status_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_added_at_idx": {
+ "name": "books_library_added_at_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"added_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_visible_added_id_idx": {
+ "name": "books_library_visible_added_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"added_at\" desc",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_id_idx": {
+ "name": "books_library_author_sort_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" asc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "books_library_author_sort_desc_id_idx": {
+ "name": "books_library_author_sort_desc_id_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "\"primary_author_sort_name\" desc nulls last",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"books\".\"status\" <> 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "books_library_id_libraries_id_fk": {
+ "name": "books_library_id_libraries_id_fk",
+ "tableFrom": "books",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_id_library_folders_id_fk": {
+ "name": "books_library_folder_id_library_folders_id_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "books_primary_file_id_book_files_id_fk": {
+ "name": "books_primary_file_id_book_files_id_fk",
+ "tableFrom": "books",
+ "tableTo": "book_files",
+ "columnsFrom": ["primary_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "books_library_folder_library_fk": {
+ "name": "books_library_folder_library_fk",
+ "tableFrom": "books",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id", "library_id"],
+ "columnsTo": ["id", "library_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "books_id_library_folder_id_unique": {
+ "name": "books_id_library_folder_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "library_folder_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "books_status_chk": {
+ "name": "books_status_chk",
+ "value": "\"books\".\"status\" in ('present', 'missing', 'processing')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.collection_books": {
+ "name": "collection_books",
+ "schema": "",
+ "columns": {
+ "collection_id": {
+ "name": "collection_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_at": {
+ "name": "added_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collection_books_book_id_idx": {
+ "name": "collection_books_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collection_books_collection_id_collections_id_fk": {
+ "name": "collection_books_collection_id_collections_id_fk",
+ "tableFrom": "collection_books",
+ "tableTo": "collections",
+ "columnsFrom": ["collection_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "collection_books_book_id_books_id_fk": {
+ "name": "collection_books_book_id_books_id_fk",
+ "tableFrom": "collection_books",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "collection_books_collection_id_book_id_pk": {
+ "name": "collection_books_collection_id_book_id_pk",
+ "columns": ["collection_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.collections": {
+ "name": "collections",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_to_kobo": {
+ "name": "sync_to_kobo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "collections_user_name_uidx": {
+ "name": "collections_user_name_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "collections_user_display_name_idx": {
+ "name": "collections_user_display_name_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "collections_user_id_users_id_fk": {
+ "name": "collections_user_id_users_id_fk",
+ "tableFrom": "collections",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.smart_scopes": {
+ "name": "smart_scopes",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "filter": {
+ "name": "filter",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_sort": {
+ "name": "default_sort",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'[]'::jsonb"
+ },
+ "is_public": {
+ "name": "is_public",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "smart_scopes_user_id_users_id_fk": {
+ "name": "smart_scopes_user_id_users_id_fk",
+ "tableFrom": "smart_scopes",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "smart_scopes_user_id_name_unique": {
+ "name": "smart_scopes_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_series": {
+ "name": "book_series",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "normalized_name": {
+ "name": "normalized_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_series_normalized_name_uidx": {
+ "name": "book_series_normalized_name_uidx",
+ "columns": [
+ {
+ "expression": "normalized_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_series_name_trgm_idx": {
+ "name": "book_series_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "book_series_name_lower_idx": {
+ "name": "book_series_name_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(\"name\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.author_enrichment_queue": {
+ "name": "author_enrichment_queue",
+ "schema": "",
+ "columns": {
+ "author_id": {
+ "name": "author_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unknown'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_attempt_at": {
+ "name": "next_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_success_at": {
+ "name": "last_success_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_http_status": {
+ "name": "last_http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "author_enrichment_queue_status_next_attempt_idx": {
+ "name": "author_enrichment_queue_status_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "author_enrichment_queue_next_attempt_idx": {
+ "name": "author_enrichment_queue_next_attempt_idx",
+ "columns": [
+ {
+ "expression": "next_attempt_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "author_enrichment_queue_single_processing_idx": {
+ "name": "author_enrichment_queue_single_processing_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"author_enrichment_queue\".\"status\" = 'processing'",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "author_enrichment_queue_author_id_authors_id_fk": {
+ "name": "author_enrichment_queue_author_id_authors_id_fk",
+ "tableFrom": "author_enrichment_queue",
+ "tableTo": "authors",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "author_enrichment_queue_status_chk": {
+ "name": "author_enrichment_queue_status_chk",
+ "value": "\"author_enrichment_queue\".\"status\" in ('queued', 'processing', 'rate_limited', 'failed', 'done')"
+ },
+ "author_enrichment_queue_reason_chk": {
+ "name": "author_enrichment_queue_reason_chk",
+ "value": "\"author_enrichment_queue\".\"reason\" in ('unknown', 'metadata_replace', 'manual_backfill', 'manual_backfill_all', 'author_rename', 'author_merge_target')"
+ },
+ "author_enrichment_queue_attempt_count_nonnegative_chk": {
+ "name": "author_enrichment_queue_attempt_count_nonnegative_chk",
+ "value": "\"author_enrichment_queue\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.authors": {
+ "name": "authors",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_name": {
+ "name": "sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "has_photo": {
+ "name": "has_photo",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "last_enriched_at": {
+ "name": "last_enriched_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "authors_name_trgm_idx": {
+ "name": "authors_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "authors_name_unique": {
+ "name": "authors_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_authors": {
+ "name": "book_authors",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "author_id": {
+ "name": "author_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "book_authors_author_id_idx": {
+ "name": "book_authors_author_id_idx",
+ "columns": [
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_authors_book_display_idx": {
+ "name": "book_authors_book_display_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_authors_book_display_author_idx": {
+ "name": "book_authors_book_display_author_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "author_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_authors_book_id_books_id_fk": {
+ "name": "book_authors_book_id_books_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_authors_author_id_authors_id_fk": {
+ "name": "book_authors_author_id_authors_id_fk",
+ "tableFrom": "book_authors",
+ "tableTo": "authors",
+ "columnsFrom": ["author_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_authors_book_id_author_id_pk": {
+ "name": "book_authors_book_id_author_id_pk",
+ "columns": ["book_id", "author_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_genres": {
+ "name": "book_genres",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "genre_id": {
+ "name": "genre_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_genres_genre_id_idx": {
+ "name": "book_genres_genre_id_idx",
+ "columns": [
+ {
+ "expression": "genre_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_genres_book_id_books_id_fk": {
+ "name": "book_genres_book_id_books_id_fk",
+ "tableFrom": "book_genres",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_genres_genre_id_genres_id_fk": {
+ "name": "book_genres_genre_id_genres_id_fk",
+ "tableFrom": "book_genres",
+ "tableTo": "genres",
+ "columnsFrom": ["genre_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_genres_book_id_genre_id_pk": {
+ "name": "book_genres_book_id_genre_id_pk",
+ "columns": ["book_id", "genre_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_metadata": {
+ "name": "book_metadata",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subtitle": {
+ "name": "subtitle",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "description": {
+ "name": "description",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn10": {
+ "name": "isbn10",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "isbn13": {
+ "name": "isbn13",
+ "type": "varchar(13)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "publisher": {
+ "name": "publisher",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "published_year": {
+ "name": "published_year",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "language": {
+ "name": "language",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_count": {
+ "name": "page_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_id": {
+ "name": "series_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_name": {
+ "name": "series_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "series_index": {
+ "name": "series_index",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "rating": {
+ "name": "rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_source": {
+ "name": "cover_source",
+ "type": "varchar(9)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "google_books_id": {
+ "name": "google_books_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "goodreads_id": {
+ "name": "goodreads_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "amazon_id": {
+ "name": "amazon_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_id": {
+ "name": "hardcover_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "open_library_id": {
+ "name": "open_library_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "itunes_id": {
+ "name": "itunes_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_id": {
+ "name": "kobo_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_score": {
+ "name": "metadata_score",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_metadata_fetch_at": {
+ "name": "last_metadata_fetch_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "embedding": {
+ "name": "embedding",
+ "type": "vector(256)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_written_at": {
+ "name": "last_written_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "abridged": {
+ "name": "abridged",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "audible_id": {
+ "name": "audible_id",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "comicvine_id": {
+ "name": "comicvine_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "ranobedb_id": {
+ "name": "ranobedb_id",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "lubimyczytac_id": {
+ "name": "lubimyczytac_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapters": {
+ "name": "chapters",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "locked_fields": {
+ "name": "locked_fields",
+ "type": "text[]",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{}'::text[]"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bm_title_trgm_idx": {
+ "name": "bm_title_trgm_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_title_lower_idx": {
+ "name": "bm_title_lower_idx",
+ "columns": [
+ {
+ "expression": "lower(\"title\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_title_book_id_idx": {
+ "name": "bm_title_book_id_idx",
+ "columns": [
+ {
+ "expression": "title",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_trgm_idx": {
+ "name": "bm_series_trgm_idx",
+ "columns": [
+ {
+ "expression": "series_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_series_id_idx": {
+ "name": "bm_series_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_id_index_book_id_idx": {
+ "name": "bm_series_id_index_book_id_idx",
+ "columns": [
+ {
+ "expression": "series_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "series_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_name_lower_btrim_idx": {
+ "name": "bm_series_name_lower_btrim_idx",
+ "columns": [
+ {
+ "expression": "lower(btrim(\"series_name\"))",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_publisher_trgm_idx": {
+ "name": "bm_publisher_trgm_idx",
+ "columns": [
+ {
+ "expression": "publisher",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_language_idx": {
+ "name": "bm_language_idx",
+ "columns": [
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_language_trgm_idx": {
+ "name": "bm_language_trgm_idx",
+ "columns": [
+ {
+ "expression": "language",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ },
+ "bm_published_year_idx": {
+ "name": "bm_published_year_idx",
+ "columns": [
+ {
+ "expression": "published_year",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_series_name_index_idx": {
+ "name": "bm_series_name_index_idx",
+ "columns": [
+ {
+ "expression": "series_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "series_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_isbn10_idx": {
+ "name": "bm_isbn10_idx",
+ "columns": [
+ {
+ "expression": "isbn10",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_isbn13_idx": {
+ "name": "bm_isbn13_idx",
+ "columns": [
+ {
+ "expression": "isbn13",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bm_embedding_hnsw_cosine_idx": {
+ "name": "bm_embedding_hnsw_cosine_idx",
+ "columns": [
+ {
+ "expression": "\"embedding\" vector_cosine_ops",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "hnsw",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_metadata_book_id_books_id_fk": {
+ "name": "book_metadata_book_id_books_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_metadata_series_id_book_series_id_fk": {
+ "name": "book_metadata_series_id_book_series_id_fk",
+ "tableFrom": "book_metadata",
+ "tableTo": "book_series",
+ "columnsFrom": ["series_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_metadata_rating_range_chk": {
+ "name": "book_metadata_rating_range_chk",
+ "value": "\"book_metadata\".\"rating\" is null or (\"book_metadata\".\"rating\" >= 1 and \"book_metadata\".\"rating\" <= 10)"
+ },
+ "book_metadata_published_year_range_chk": {
+ "name": "book_metadata_published_year_range_chk",
+ "value": "\"book_metadata\".\"published_year\" is null or (\"book_metadata\".\"published_year\" >= 1000 and \"book_metadata\".\"published_year\" <= 2200)"
+ },
+ "book_metadata_page_count_nonnegative_chk": {
+ "name": "book_metadata_page_count_nonnegative_chk",
+ "value": "\"book_metadata\".\"page_count\" is null or \"book_metadata\".\"page_count\" >= 0"
+ },
+ "book_metadata_duration_seconds_nonnegative_chk": {
+ "name": "book_metadata_duration_seconds_nonnegative_chk",
+ "value": "\"book_metadata\".\"duration_seconds\" is null or \"book_metadata\".\"duration_seconds\" >= 0"
+ },
+ "book_metadata_cover_source_chk": {
+ "name": "book_metadata_cover_source_chk",
+ "value": "\"book_metadata\".\"cover_source\" is null or \"book_metadata\".\"cover_source\" in ('extracted', 'custom')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_metadata_fetch_queue": {
+ "name": "book_metadata_fetch_queue",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'queued'"
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'manual_trigger'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "last_attempt_at": {
+ "name": "last_attempt_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_error": {
+ "name": "last_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_http_status": {
+ "name": "last_http_status",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bmfq_status_idx": {
+ "name": "bmfq_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bmfq_created_at_idx": {
+ "name": "bmfq_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bmfq_status_created_book_idx": {
+ "name": "bmfq_status_created_book_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_metadata_fetch_queue_book_id_books_id_fk": {
+ "name": "book_metadata_fetch_queue_book_id_books_id_fk",
+ "tableFrom": "book_metadata_fetch_queue",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_metadata_fetch_queue_status_chk": {
+ "name": "book_metadata_fetch_queue_status_chk",
+ "value": "\"book_metadata_fetch_queue\".\"status\" in ('queued', 'processing', 'failed')"
+ },
+ "book_metadata_fetch_queue_reason_chk": {
+ "name": "book_metadata_fetch_queue_reason_chk",
+ "value": "\"book_metadata_fetch_queue\".\"reason\" in ('event_import', 'manual_trigger', 'manual_retry')"
+ },
+ "book_metadata_fetch_queue_attempt_count_nonnegative_chk": {
+ "name": "book_metadata_fetch_queue_attempt_count_nonnegative_chk",
+ "value": "\"book_metadata_fetch_queue\".\"attempt_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_tags": {
+ "name": "book_tags",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "tag_id": {
+ "name": "tag_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "book_tags_tag_id_idx": {
+ "name": "book_tags_tag_id_idx",
+ "columns": [
+ {
+ "expression": "tag_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_tags_book_id_books_id_fk": {
+ "name": "book_tags_book_id_books_id_fk",
+ "tableFrom": "book_tags",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_tags_tag_id_tags_id_fk": {
+ "name": "book_tags_tag_id_tags_id_fk",
+ "tableFrom": "book_tags",
+ "tableTo": "tags",
+ "columnsFrom": ["tag_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_tags_book_id_tag_id_pk": {
+ "name": "book_tags_book_id_tag_id_pk",
+ "columns": ["book_id", "tag_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.genres": {
+ "name": "genres",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "genres_name_trgm_idx": {
+ "name": "genres_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "genres_name_unique": {
+ "name": "genres_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.tags": {
+ "name": "tags",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "tags_name_trgm_idx": {
+ "name": "tags_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "tags_name_unique": {
+ "name": "tags_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_narrators": {
+ "name": "book_narrators",
+ "schema": "",
+ "columns": {
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "narrator_id": {
+ "name": "narrator_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ }
+ },
+ "indexes": {
+ "book_narrators_narrator_id_idx": {
+ "name": "book_narrators_narrator_id_idx",
+ "columns": [
+ {
+ "expression": "narrator_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_narrators_book_id_books_id_fk": {
+ "name": "book_narrators_book_id_books_id_fk",
+ "tableFrom": "book_narrators",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "book_narrators_narrator_id_narrators_id_fk": {
+ "name": "book_narrators_narrator_id_narrators_id_fk",
+ "tableFrom": "book_narrators",
+ "tableTo": "narrators",
+ "columnsFrom": ["narrator_id"],
+ "columnsTo": ["id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "book_narrators_book_id_narrator_id_pk": {
+ "name": "book_narrators_book_id_narrator_id_pk",
+ "columns": ["book_id", "narrator_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.narrators": {
+ "name": "narrators",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_name": {
+ "name": "sort_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "narrators_name_trgm_idx": {
+ "name": "narrators_name_trgm_idx",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last",
+ "opclass": "gin_trgm_ops"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "gin",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "narrators_name_unique": {
+ "name": "narrators_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.library_dir_scan_state": {
+ "name": "library_dir_scan_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_folder_id": {
+ "name": "library_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dir_path": {
+ "name": "dir_path",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_mtime_ms": {
+ "name": "last_seen_mtime_ms",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "library_dir_scan_state_folder_dir_uidx": {
+ "name": "library_dir_scan_state_folder_dir_uidx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "dir_path",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "library_dir_scan_state_folder_idx": {
+ "name": "library_dir_scan_state_folder_idx",
+ "columns": [
+ {
+ "expression": "library_folder_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "library_dir_scan_state_library_folder_id_library_folders_id_fk": {
+ "name": "library_dir_scan_state_library_folder_id_library_folders_id_fk",
+ "tableFrom": "library_dir_scan_state",
+ "tableTo": "library_folders",
+ "columnsFrom": ["library_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.scan_jobs": {
+ "name": "scan_jobs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'running'"
+ },
+ "triggered_by": {
+ "name": "triggered_by",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "added_count": {
+ "name": "added_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_count": {
+ "name": "updated_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "missing_count": {
+ "name": "missing_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "completed_at": {
+ "name": "completed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "scan_jobs_library_status_idx": {
+ "name": "scan_jobs_library_status_idx",
+ "columns": [
+ {
+ "expression": "library_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "scan_jobs_library_id_libraries_id_fk": {
+ "name": "scan_jobs_library_id_libraries_id_fk",
+ "tableFrom": "scan_jobs",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "scan_jobs_status_chk": {
+ "name": "scan_jobs_status_chk",
+ "value": "\"scan_jobs\".\"status\" in ('running', 'completed', 'failed')"
+ },
+ "scan_jobs_triggered_by_chk": {
+ "name": "scan_jobs_triggered_by_chk",
+ "value": "\"scan_jobs\".\"triggered_by\" in ('manual', 'watcher', 'schedule')"
+ },
+ "scan_jobs_added_count_nonnegative_chk": {
+ "name": "scan_jobs_added_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"added_count\" >= 0"
+ },
+ "scan_jobs_updated_count_nonnegative_chk": {
+ "name": "scan_jobs_updated_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"updated_count\" >= 0"
+ },
+ "scan_jobs_missing_count_nonnegative_chk": {
+ "name": "scan_jobs_missing_count_nonnegative_chk",
+ "value": "\"scan_jobs\".\"missing_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.annotations": {
+ "name": "annotations",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "text": {
+ "name": "text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "color": {
+ "name": "color",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'yellow'"
+ },
+ "style": {
+ "name": "style",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'highlight'"
+ },
+ "note": {
+ "name": "note",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_title": {
+ "name": "chapter_title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "annotations_user_id_idx": {
+ "name": "annotations_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "annotations_user_book_idx": {
+ "name": "annotations_user_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "annotations_user_id_users_id_fk": {
+ "name": "annotations_user_id_users_id_fk",
+ "tableFrom": "annotations",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "annotations_book_id_books_id_fk": {
+ "name": "annotations_book_id_books_id_fk",
+ "tableFrom": "annotations",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "annotations_style_chk": {
+ "name": "annotations_style_chk",
+ "value": "\"annotations\".\"style\" in ('highlight', 'underline', 'strikethrough', 'squiggly')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.audiobook_progress": {
+ "name": "audiobook_progress",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "current_file_id": {
+ "name": "current_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "abp_user_id_idx": {
+ "name": "abp_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "audiobook_progress_user_id_users_id_fk": {
+ "name": "audiobook_progress_user_id_users_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "audiobook_progress_book_id_books_id_fk": {
+ "name": "audiobook_progress_book_id_books_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "audiobook_progress_current_file_id_book_files_id_fk": {
+ "name": "audiobook_progress_current_file_id_book_files_id_fk",
+ "tableFrom": "audiobook_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["current_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "audiobook_progress_user_id_book_id_pk": {
+ "name": "audiobook_progress_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "audiobook_progress_percentage_range_chk": {
+ "name": "audiobook_progress_percentage_range_chk",
+ "value": "\"audiobook_progress\".\"percentage\" >= 0 and \"audiobook_progress\".\"percentage\" <= 100"
+ },
+ "audiobook_progress_position_seconds_nonnegative_chk": {
+ "name": "audiobook_progress_position_seconds_nonnegative_chk",
+ "value": "\"audiobook_progress\".\"position_seconds\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.bookmarks": {
+ "name": "bookmarks",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "bookmarks_user_book_idx": {
+ "name": "bookmarks_user_book_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_user_book_cfi_uidx": {
+ "name": "bookmarks_user_book_cfi_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "cfi",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"bookmarks\".\"cfi\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "bookmarks_user_book_pos_uidx": {
+ "name": "bookmarks_user_book_pos_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "position_seconds",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"bookmarks\".\"position_seconds\" is not null and \"bookmarks\".\"cfi\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "bookmarks_user_id_users_id_fk": {
+ "name": "bookmarks_user_id_users_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "bookmarks_book_id_books_id_fk": {
+ "name": "bookmarks_book_id_books_id_fk",
+ "tableFrom": "bookmarks",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reader_default_preferences": {
+ "name": "reader_default_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format_group": {
+ "name": "format_group",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "rdp_user_format_idx": {
+ "name": "rdp_user_format_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "format_group",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reader_default_preferences_user_id_users_id_fk": {
+ "name": "reader_default_preferences_user_id_users_id_fk",
+ "tableFrom": "reader_default_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reader_default_preferences_format_group_chk": {
+ "name": "reader_default_preferences_format_group_chk",
+ "value": "\"reader_default_preferences\".\"format_group\" in ('epub', 'pdf', 'cbx', 'audio')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reader_preferences": {
+ "name": "reader_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "settings": {
+ "name": "settings",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "rp_user_file_idx": {
+ "name": "rp_user_file_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reader_preferences_user_id_users_id_fk": {
+ "name": "reader_preferences_user_id_users_id_fk",
+ "tableFrom": "reader_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reader_preferences_book_file_id_book_files_id_fk": {
+ "name": "reader_preferences_book_file_id_book_files_id_fk",
+ "tableFrom": "reader_preferences",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.reading_progress": {
+ "name": "reading_progress",
+ "schema": "",
+ "columns": {
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "cfi": {
+ "name": "cfi",
+ "type": "varchar(2000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "page_number": {
+ "name": "page_number",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "position_seconds": {
+ "name": "position_seconds",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_source": {
+ "name": "kobo_location_source",
+ "type": "varchar(4096)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_type": {
+ "name": "kobo_location_type",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_location_value": {
+ "name": "kobo_location_value",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "kobo_content_source_progress_percent": {
+ "name": "kobo_content_source_progress_percent",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "koreader_progress": {
+ "name": "koreader_progress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "reading_progress_user_id_idx": {
+ "name": "reading_progress_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "reading_progress_user_updated_at_idx": {
+ "name": "reading_progress_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_progress_book_file_id_book_files_id_fk": {
+ "name": "reading_progress_book_file_id_book_files_id_fk",
+ "tableFrom": "reading_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_progress_user_id_users_id_fk": {
+ "name": "reading_progress_user_id_users_id_fk",
+ "tableFrom": "reading_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "reading_progress_book_file_id_user_id_pk": {
+ "name": "reading_progress_book_file_id_user_id_pk",
+ "columns": ["book_file_id", "user_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reading_progress_percentage_range_chk": {
+ "name": "reading_progress_percentage_range_chk",
+ "value": "\"reading_progress\".\"percentage\" >= 0 and \"reading_progress\".\"percentage\" <= 100"
+ },
+ "reading_progress_page_number_nonnegative_chk": {
+ "name": "reading_progress_page_number_nonnegative_chk",
+ "value": "\"reading_progress\".\"page_number\" is null or \"reading_progress\".\"page_number\" >= 0"
+ },
+ "reading_progress_position_seconds_nonnegative_chk": {
+ "name": "reading_progress_position_seconds_nonnegative_chk",
+ "value": "\"reading_progress\".\"position_seconds\" is null or \"reading_progress\".\"position_seconds\" >= 0"
+ },
+ "reading_progress_kobo_content_source_progress_range_chk": {
+ "name": "reading_progress_kobo_content_source_progress_range_chk",
+ "value": "\"reading_progress\".\"kobo_content_source_progress_percent\" is null or (\"reading_progress\".\"kobo_content_source_progress_percent\" >= 0 and \"reading_progress\".\"kobo_content_source_progress_percent\" <= 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.reading_sessions": {
+ "name": "reading_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "session_id": {
+ "name": "session_id",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "ended_at": {
+ "name": "ended_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "duration_seconds": {
+ "name": "duration_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "progress_delta": {
+ "name": "progress_delta",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "end_progress": {
+ "name": "end_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "rs_user_session_id_uidx": {
+ "name": "rs_user_session_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_started_at_idx": {
+ "name": "rs_user_started_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_book_file_started_at_idx": {
+ "name": "rs_book_file_started_at_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "started_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "rs_user_book_file_idx": {
+ "name": "rs_user_book_file_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "reading_sessions_user_id_users_id_fk": {
+ "name": "reading_sessions_user_id_users_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "reading_sessions_book_file_id_book_files_id_fk": {
+ "name": "reading_sessions_book_file_id_book_files_id_fk",
+ "tableFrom": "reading_sessions",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "reading_sessions_duration_seconds_nonnegative_chk": {
+ "name": "reading_sessions_duration_seconds_nonnegative_chk",
+ "value": "\"reading_sessions\".\"duration_seconds\" >= 0"
+ },
+ "reading_sessions_end_progress_range_chk": {
+ "name": "reading_sessions_end_progress_range_chk",
+ "value": "\"reading_sessions\".\"end_progress\" is null or (\"reading_sessions\".\"end_progress\" >= 0 and \"reading_sessions\".\"end_progress\" <= 100)"
+ },
+ "reading_sessions_ended_after_started_chk": {
+ "name": "reading_sessions_ended_after_started_chk",
+ "value": "\"reading_sessions\".\"ended_at\" >= \"reading_sessions\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_ratings": {
+ "name": "user_book_ratings",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rating": {
+ "name": "rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubr_user_id_idx": {
+ "name": "ubr_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubr_book_id_idx": {
+ "name": "ubr_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubr_book_user_rating_idx": {
+ "name": "ubr_book_user_rating_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_ratings_user_id_users_id_fk": {
+ "name": "user_book_ratings_user_id_users_id_fk",
+ "tableFrom": "user_book_ratings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_ratings_book_id_books_id_fk": {
+ "name": "user_book_ratings_book_id_books_id_fk",
+ "tableFrom": "user_book_ratings",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_ratings_user_id_book_id_pk": {
+ "name": "user_book_ratings_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_ratings_rating_range_chk": {
+ "name": "user_book_ratings_rating_range_chk",
+ "value": "\"user_book_ratings\".\"rating\" >= 1 and \"user_book_ratings\".\"rating\" <= 5"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_book_status": {
+ "name": "user_book_status",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'unread'"
+ },
+ "source": {
+ "name": "source",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'auto'"
+ },
+ "started_at": {
+ "name": "started_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "finished_at": {
+ "name": "finished_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "ubs_user_id_idx": {
+ "name": "ubs_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubs_user_status_idx": {
+ "name": "ubs_user_status_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "ubs_book_user_idx": {
+ "name": "ubs_book_user_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_book_status_user_id_users_id_fk": {
+ "name": "user_book_status_user_id_users_id_fk",
+ "tableFrom": "user_book_status",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_book_status_book_id_books_id_fk": {
+ "name": "user_book_status_book_id_books_id_fk",
+ "tableFrom": "user_book_status",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_book_status_user_id_book_id_pk": {
+ "name": "user_book_status_user_id_book_id_pk",
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_book_status_status_chk": {
+ "name": "user_book_status_status_chk",
+ "value": "\"user_book_status\".\"status\" in ('unread', 'want_to_read', 'reading', 'on_hold', 'rereading', 'read', 'skimmed', 'abandoned')"
+ },
+ "user_book_status_source_chk": {
+ "name": "user_book_status_source_chk",
+ "value": "\"user_book_status\".\"source\" in ('auto', 'manual')"
+ },
+ "user_book_status_finished_after_started_chk": {
+ "name": "user_book_status_finished_after_started_chk",
+ "value": "\"user_book_status\".\"finished_at\" is null or \"user_book_status\".\"started_at\" is null or \"user_book_status\".\"finished_at\" >= \"user_book_status\".\"started_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_reading_daily_stats": {
+ "name": "user_reading_daily_stats",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "library_id": {
+ "name": "library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "day": {
+ "name": "day",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_seconds": {
+ "name": "reading_seconds",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "progress_delta": {
+ "name": "progress_delta",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "sessions_count": {
+ "name": "sessions_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "urds_user_day_idx": {
+ "name": "urds_user_day_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "day",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_reading_daily_stats_user_id_users_id_fk": {
+ "name": "user_reading_daily_stats_user_id_users_id_fk",
+ "tableFrom": "user_reading_daily_stats",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_reading_daily_stats_library_id_libraries_id_fk": {
+ "name": "user_reading_daily_stats_library_id_libraries_id_fk",
+ "tableFrom": "user_reading_daily_stats",
+ "tableTo": "libraries",
+ "columnsFrom": ["library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "user_reading_daily_stats_user_id_library_id_day_pk": {
+ "name": "user_reading_daily_stats_user_id_library_id_day_pk",
+ "columns": ["user_id", "library_id", "day"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_reading_daily_stats_reading_seconds_nonnegative_chk": {
+ "name": "user_reading_daily_stats_reading_seconds_nonnegative_chk",
+ "value": "\"user_reading_daily_stats\".\"reading_seconds\" >= 0"
+ },
+ "user_reading_daily_stats_sessions_count_nonnegative_chk": {
+ "name": "user_reading_daily_stats_sessions_count_nonnegative_chk",
+ "value": "\"user_reading_daily_stats\".\"sessions_count\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.oidc_group_mappings": {
+ "name": "oidc_group_mappings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_group_claim": {
+ "name": "oidc_group_claim",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_group_mappings_provider_claim_uidx": {
+ "name": "oidc_group_mappings_provider_claim_uidx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_group_claim",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_group_mappings_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_group_mappings_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_group_mappings",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_identities": {
+ "name": "oidc_identities",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "linked_at": {
+ "name": "linked_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_identities_provider_subject_uidx": {
+ "name": "oidc_identities_provider_subject_uidx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_user_provider_uidx": {
+ "name": "oidc_identities_user_provider_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_issuer_subject_uidx": {
+ "name": "oidc_identities_issuer_subject_uidx",
+ "columns": [
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_identities_user_id_idx": {
+ "name": "oidc_identities_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_identities_user_id_users_id_fk": {
+ "name": "oidc_identities_user_id_users_id_fk",
+ "tableFrom": "oidc_identities",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_identities_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_identities_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_identities",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_permission_grants": {
+ "name": "oidc_permission_grants",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "permission_name": {
+ "name": "permission_name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "oidc_permission_grants_user_id_users_id_fk": {
+ "name": "oidc_permission_grants_user_id_users_id_fk",
+ "tableFrom": "oidc_permission_grants",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_permission_grants_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_permission_grants_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_permission_grants",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "oidc_permission_grants_user_id_provider_id_permission_name_pk": {
+ "name": "oidc_permission_grants_user_id_provider_id_permission_name_pk",
+ "columns": ["user_id", "provider_id", "permission_name"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_providers": {
+ "name": "oidc_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "slug": {
+ "name": "slug",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "display_name": {
+ "name": "display_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "issuer_uri": {
+ "name": "issuer_uri",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_id": {
+ "name": "client_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "client_secret": {
+ "name": "client_secret",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "scopes": {
+ "name": "scopes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'openid profile email'"
+ },
+ "icon_url": {
+ "name": "icon_url",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "claim_mapping": {
+ "name": "claim_mapping",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"username\":\"preferred_username\",\"name\":\"name\",\"email\":\"email\",\"groups\":\"groups\"}'::jsonb"
+ },
+ "auto_provision": {
+ "name": "auto_provision",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'{\"enabled\":false,\"allowLocalLinking\":false,\"defaultPermissionNames\":[]}'::jsonb"
+ },
+ "display_order": {
+ "name": "display_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "oidc_providers_enabled_order_idx": {
+ "name": "oidc_providers_enabled_order_idx",
+ "columns": [
+ {
+ "expression": "enabled",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "display_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "oidc_providers_slug_unique": {
+ "name": "oidc_providers_slug_unique",
+ "nullsNotDistinct": false,
+ "columns": ["slug"]
+ },
+ "oidc_providers_issuer_uri_unique": {
+ "name": "oidc_providers_issuer_uri_unique",
+ "nullsNotDistinct": false,
+ "columns": ["issuer_uri"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_sessions": {
+ "name": "oidc_sessions",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "oidc_subject": {
+ "name": "oidc_subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_issuer": {
+ "name": "oidc_issuer",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "oidc_session_id": {
+ "name": "oidc_session_id",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "id_token_hint": {
+ "name": "id_token_hint",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "idp_refresh_token": {
+ "name": "idp_refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "revoked": {
+ "name": "revoked",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oidc_sessions_user_active_idx": {
+ "name": "oidc_sessions_user_active_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"oidc_sessions\".\"revoked\" = false",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_subject_issuer_idx": {
+ "name": "oidc_sessions_subject_issuer_idx",
+ "columns": [
+ {
+ "expression": "oidc_subject",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "oidc_issuer",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_sid_idx": {
+ "name": "oidc_sessions_sid_idx",
+ "columns": [
+ {
+ "expression": "oidc_session_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_expires_at_idx": {
+ "name": "oidc_sessions_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "oidc_sessions_provider_id_idx": {
+ "name": "oidc_sessions_provider_id_idx",
+ "columns": [
+ {
+ "expression": "provider_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_sessions_user_id_users_id_fk": {
+ "name": "oidc_sessions_user_id_users_id_fk",
+ "tableFrom": "oidc_sessions",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "oidc_sessions_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_sessions_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_sessions",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_states": {
+ "name": "oidc_states",
+ "schema": "",
+ "columns": {
+ "state": {
+ "name": "state",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "meta": {
+ "name": "meta",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "oidc_states_expires_at_idx": {
+ "name": "oidc_states_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "oidc_states_provider_id_oidc_providers_id_fk": {
+ "name": "oidc_states_provider_id_oidc_providers_id_fk",
+ "tableFrom": "oidc_states",
+ "tableTo": "oidc_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.oidc_used_jtis": {
+ "name": "oidc_used_jtis",
+ "schema": "",
+ "columns": {
+ "jti": {
+ "name": "jti",
+ "type": "text",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "oidc_used_jtis_expires_at_idx": {
+ "name": "oidc_used_jtis_expires_at_idx",
+ "columns": [
+ {
+ "expression": "expires_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.opds_users": {
+ "name": "opds_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "opds_sort_order",
+ "typeSchema": "public",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'recent'"
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "opds_users_username_lower_uidx": {
+ "name": "opds_users_username_lower_uidx",
+ "columns": [
+ {
+ "expression": "lower(\"username\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "opds_users_user_id_users_id_fk": {
+ "name": "opds_users_user_id_users_id_fk",
+ "tableFrom": "opds_users",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "opds_users_username_unique": {
+ "name": "opds_users_username_unique",
+ "nullsNotDistinct": false,
+ "columns": ["username"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_book_entitlements": {
+ "name": "kobo_book_entitlements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "cover_image_id": {
+ "name": "cover_image_id",
+ "type": "uuid",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "gen_random_uuid()"
+ },
+ "needs_legacy_numeric_removal": {
+ "name": "needs_legacy_numeric_removal",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_book_entitlements_user_id_idx": {
+ "name": "kobo_book_entitlements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "kobo_book_entitlements_book_id_idx": {
+ "name": "kobo_book_entitlements_book_id_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_book_entitlements_user_id_users_id_fk": {
+ "name": "kobo_book_entitlements_user_id_users_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_book_entitlements_book_id_books_id_fk": {
+ "name": "kobo_book_entitlements_book_id_books_id_fk",
+ "tableFrom": "kobo_book_entitlements",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_book_entitlements_user_book_unique": {
+ "name": "kobo_book_entitlements_user_book_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ },
+ "kobo_book_entitlements_user_entitlement_unique": {
+ "name": "kobo_book_entitlements_user_entitlement_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "entitlement_id"]
+ },
+ "kobo_book_entitlements_user_cover_unique": {
+ "name": "kobo_book_entitlements_user_cover_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "cover_image_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_devices": {
+ "name": "kobo_devices",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "last_seen_at": {
+ "name": "last_seen_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "kobo_devices_user_id_idx": {
+ "name": "kobo_devices_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_devices_user_id_users_id_fk": {
+ "name": "kobo_devices_user_id_users_id_fk",
+ "tableFrom": "kobo_devices",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_devices_token_unique": {
+ "name": "kobo_devices_token_unique",
+ "nullsNotDistinct": false,
+ "columns": ["token"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_library_snapshots": {
+ "name": "kobo_library_snapshots",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_library_snapshots_user_id_users_id_fk": {
+ "name": "kobo_library_snapshots_user_id_users_id_fk",
+ "tableFrom": "kobo_library_snapshots",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_library_snapshots_user_id_unique": {
+ "name": "kobo_library_snapshots_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_reading_states": {
+ "name": "kobo_reading_states",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entitlement_id": {
+ "name": "entitlement_id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at_kobo": {
+ "name": "created_at_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_modified_kobo": {
+ "name": "last_modified_kobo",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "priority_timestamp": {
+ "name": "priority_timestamp",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "current_bookmark": {
+ "name": "current_bookmark",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "statistics": {
+ "name": "statistics",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status_info": {
+ "name": "status_info",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_reading_states_user_id_users_id_fk": {
+ "name": "kobo_reading_states_user_id_users_id_fk",
+ "tableFrom": "kobo_reading_states",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_reading_states_book_id_books_id_fk": {
+ "name": "kobo_reading_states_book_id_books_id_fk",
+ "tableFrom": "kobo_reading_states",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_reading_states_user_id_book_id_unique": {
+ "name": "kobo_reading_states_user_id_book_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_snapshot_books": {
+ "name": "kobo_snapshot_books",
+ "schema": "",
+ "columns": {
+ "snapshot_id": {
+ "name": "snapshot_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "synced": {
+ "name": "synced",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "pending_delete": {
+ "name": "pending_delete",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_new": {
+ "name": "is_new",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "removed_by_device": {
+ "name": "removed_by_device",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "delivery_hash": {
+ "name": "delivery_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_hash": {
+ "name": "metadata_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "kobo_snapshot_books_snapshot_synced_book_idx": {
+ "name": "kobo_snapshot_books_snapshot_synced_book_idx",
+ "columns": [
+ {
+ "expression": "snapshot_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "synced",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "kobo_snapshot_books_snapshot_id_kobo_library_snapshots_id_fk": {
+ "name": "kobo_snapshot_books_snapshot_id_kobo_library_snapshots_id_fk",
+ "tableFrom": "kobo_snapshot_books",
+ "tableTo": "kobo_library_snapshots",
+ "columnsFrom": ["snapshot_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "kobo_snapshot_books_book_id_books_id_fk": {
+ "name": "kobo_snapshot_books_book_id_books_id_fk",
+ "tableFrom": "kobo_snapshot_books",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "kobo_snapshot_books_snapshot_id_book_id_pk": {
+ "name": "kobo_snapshot_books_snapshot_id_book_id_pk",
+ "columns": ["snapshot_id", "book_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.kobo_sync_settings": {
+ "name": "kobo_sync_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reading_threshold": {
+ "name": "reading_threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 1
+ },
+ "finished_threshold": {
+ "name": "finished_threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 99
+ },
+ "convert_to_kepub": {
+ "name": "convert_to_kepub",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "force_enable_hyphenation": {
+ "name": "force_enable_hyphenation",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "kepub_conversion_limit_mb": {
+ "name": "kepub_conversion_limit_mb",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 100
+ },
+ "two_way_progress_sync": {
+ "name": "two_way_progress_sync",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "kobo_sync_settings_user_id_users_id_fk": {
+ "name": "kobo_sync_settings_user_id_users_id_fk",
+ "tableFrom": "kobo_sync_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "kobo_sync_settings_user_id_unique": {
+ "name": "kobo_sync_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "kobo_sync_settings_reading_threshold_range_chk": {
+ "name": "kobo_sync_settings_reading_threshold_range_chk",
+ "value": "\"kobo_sync_settings\".\"reading_threshold\" >= 0 and \"kobo_sync_settings\".\"reading_threshold\" <= 100"
+ },
+ "kobo_sync_settings_finished_threshold_range_chk": {
+ "name": "kobo_sync_settings_finished_threshold_range_chk",
+ "value": "\"kobo_sync_settings\".\"finished_threshold\" >= 0 and \"kobo_sync_settings\".\"finished_threshold\" <= 100"
+ },
+ "kobo_sync_settings_conversion_limit_nonnegative_chk": {
+ "name": "kobo_sync_settings_conversion_limit_nonnegative_chk",
+ "value": "\"kobo_sync_settings\".\"kepub_conversion_limit_mb\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_dock_files": {
+ "name": "book_dock_files",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "file_name": {
+ "name": "file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "absolute_path": {
+ "name": "absolute_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "embedded_metadata": {
+ "name": "embedded_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "selected_metadata": {
+ "name": "selected_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetched_metadata": {
+ "name": "fetched_metadata",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "cover_path": {
+ "name": "cover_path",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_library_id": {
+ "name": "target_library_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "target_folder_id": {
+ "name": "target_folder_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "confidence": {
+ "name": "confidence",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "fetched_metadata_sources": {
+ "name": "fetched_metadata_sources",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "metadata_edited_at": {
+ "name": "metadata_edited_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "uploaded_by": {
+ "name": "uploaded_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_dock_files_status_idx": {
+ "name": "book_dock_files_status_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_dock_files_uploaded_by_idx": {
+ "name": "book_dock_files_uploaded_by_idx",
+ "columns": [
+ {
+ "expression": "uploaded_by",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_dock_files_target_library_id_libraries_id_fk": {
+ "name": "book_dock_files_target_library_id_libraries_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "libraries",
+ "columnsFrom": ["target_library_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_dock_files_target_folder_id_library_folders_id_fk": {
+ "name": "book_dock_files_target_folder_id_library_folders_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "library_folders",
+ "columnsFrom": ["target_folder_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "book_dock_files_uploaded_by_users_id_fk": {
+ "name": "book_dock_files_uploaded_by_users_id_fk",
+ "tableFrom": "book_dock_files",
+ "tableTo": "users",
+ "columnsFrom": ["uploaded_by"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "book_dock_files_absolute_path_unique": {
+ "name": "book_dock_files_absolute_path_unique",
+ "nullsNotDistinct": false,
+ "columns": ["absolute_path"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "book_dock_files_status_chk": {
+ "name": "book_dock_files_status_chk",
+ "value": "\"book_dock_files\".\"status\" in ('pending', 'extracting', 'fetching', 'ready', 'error')"
+ },
+ "book_dock_files_confidence_range_chk": {
+ "name": "book_dock_files_confidence_range_chk",
+ "value": "\"book_dock_files\".\"confidence\" is null or (\"book_dock_files\".\"confidence\" >= 0 and \"book_dock_files\".\"confidence\" <= 100)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.file_write_log": {
+ "name": "file_write_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "fields_written": {
+ "name": "fields_written",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "duration_ms": {
+ "name": "duration_ms",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "triggered_by": {
+ "name": "triggered_by",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "written_at": {
+ "name": "written_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "fwl_book_id_written_at_idx": {
+ "name": "fwl_book_id_written_at_idx",
+ "columns": [
+ {
+ "expression": "book_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "written_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "file_write_log_book_id_books_id_fk": {
+ "name": "file_write_log_book_id_books_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "file_write_log_book_file_id_book_files_id_fk": {
+ "name": "file_write_log_book_file_id_book_files_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "file_write_log_user_id_users_id_fk": {
+ "name": "file_write_log_user_id_users_id_fk",
+ "tableFrom": "file_write_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "file_write_log_status_chk": {
+ "name": "file_write_log_status_chk",
+ "value": "\"file_write_log\".\"status\" in ('success', 'skipped', 'failed')"
+ },
+ "file_write_log_triggered_by_chk": {
+ "name": "file_write_log_triggered_by_chk",
+ "value": "\"file_write_log\".\"triggered_by\" in ('auto', 'sync')"
+ },
+ "file_write_log_duration_ms_nonnegative_chk": {
+ "name": "file_write_log_duration_ms_nonnegative_chk",
+ "value": "\"file_write_log\".\"duration_ms\" is null or \"file_write_log\".\"duration_ms\" >= 0"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_templates": {
+ "name": "email_templates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "body_text": {
+ "name": "body_text",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_system": {
+ "name": "is_system",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_templates_one_default_per_user_uidx": {
+ "name": "email_templates_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_templates\".\"is_default\" = true and \"email_templates\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_templates_system_name_unique": {
+ "name": "email_templates_system_name_unique",
+ "columns": [
+ {
+ "expression": "name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_templates\".\"user_id\" is null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_templates_user_id_users_id_fk": {
+ "name": "email_templates_user_id_users_id_fk",
+ "tableFrom": "email_templates",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_templates_user_id_name_unique": {
+ "name": "email_templates_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_providers": {
+ "name": "email_providers",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "host": {
+ "name": "host",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "port": {
+ "name": "port",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "password_enc": {
+ "name": "password_enc",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_name": {
+ "name": "from_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "from_address": {
+ "name": "from_address",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "auth": {
+ "name": "auth",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "ssl": {
+ "name": "ssl",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "start_tls": {
+ "name": "start_tls",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_shared": {
+ "name": "is_shared",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "is_system_provider": {
+ "name": "is_system_provider",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "tls_reject_unauthorized": {
+ "name": "tls_reject_unauthorized",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_providers_one_default_per_user_uidx": {
+ "name": "email_providers_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_providers\".\"is_default\" = true and \"email_providers\".\"user_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_providers_one_system_provider_uidx": {
+ "name": "email_providers_one_system_provider_uidx",
+ "columns": [
+ {
+ "expression": "is_system_provider",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_providers\".\"is_system_provider\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_providers_user_id_users_id_fk": {
+ "name": "email_providers_user_id_users_id_fk",
+ "tableFrom": "email_providers",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_providers_user_id_name_unique": {
+ "name": "email_providers_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ },
+ "email_providers_id_user_id_unique": {
+ "name": "email_providers_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "email_providers_port_range_chk": {
+ "name": "email_providers_port_range_chk",
+ "value": "\"email_providers\".\"port\" >= 1 and \"email_providers\".\"port\" <= 65535"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_recipient_group_members": {
+ "name": "email_recipient_group_members",
+ "schema": "",
+ "columns": {
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "group_id": {
+ "name": "group_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "recipient_id": {
+ "name": "recipient_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_recipient_group_members_user_id_users_id_fk": {
+ "name": "email_recipient_group_members_user_id_users_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_group_id_email_recipient_groups_id_fk": {
+ "name": "email_recipient_group_members_group_id_email_recipient_groups_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipient_groups",
+ "columnsFrom": ["group_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_recipient_id_email_recipients_id_fk": {
+ "name": "email_recipient_group_members_recipient_id_email_recipients_id_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["recipient_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_group_user_fk": {
+ "name": "email_recipient_group_members_group_user_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipient_groups",
+ "columnsFrom": ["group_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ },
+ "email_recipient_group_members_recipient_user_fk": {
+ "name": "email_recipient_group_members_recipient_user_fk",
+ "tableFrom": "email_recipient_group_members",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["recipient_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {
+ "email_recipient_group_members_group_id_recipient_id_pk": {
+ "name": "email_recipient_group_members_group_id_recipient_id_pk",
+ "columns": ["group_id", "recipient_id"]
+ }
+ },
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_recipient_groups": {
+ "name": "email_recipient_groups",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_recipient_groups_user_id_users_id_fk": {
+ "name": "email_recipient_groups_user_id_users_id_fk",
+ "tableFrom": "email_recipient_groups",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipient_groups_default_template_id_email_templates_id_fk": {
+ "name": "email_recipient_groups_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_recipient_groups",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_recipient_groups_user_id_name_unique": {
+ "name": "email_recipient_groups_user_id_name_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "name"]
+ },
+ "email_recipient_groups_id_user_id_unique": {
+ "name": "email_recipient_groups_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_recipients": {
+ "name": "email_recipients",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "is_default": {
+ "name": "is_default",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "device_type": {
+ "name": "device_type",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "preferred_format": {
+ "name": "preferred_format",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_recipients_user_email_lower_uidx": {
+ "name": "email_recipients_user_email_lower_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "lower(\"email\")",
+ "asc": true,
+ "isExpression": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_recipients_one_default_per_user_uidx": {
+ "name": "email_recipients_one_default_per_user_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"email_recipients\".\"is_default\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_recipients_user_id_users_id_fk": {
+ "name": "email_recipients_user_id_users_id_fk",
+ "tableFrom": "email_recipients",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_recipients_default_template_id_email_templates_id_fk": {
+ "name": "email_recipients_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_recipients",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_recipients_user_id_email_unique": {
+ "name": "email_recipients_user_id_email_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "email"]
+ },
+ "email_recipients_id_user_id_unique": {
+ "name": "email_recipients_id_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["id", "user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {
+ "email_recipients_device_type_chk": {
+ "name": "email_recipients_device_type_chk",
+ "value": "\"email_recipients\".\"device_type\" is null or \"email_recipients\".\"device_type\" in ('kindle', 'kobo', 'other')"
+ },
+ "email_recipients_preferred_format_chk": {
+ "name": "email_recipients_preferred_format_chk",
+ "value": "\"email_recipients\".\"preferred_format\" is null or \"email_recipients\".\"preferred_format\" in ('epub', 'pdf', 'mobi', 'azw3', 'cbz', 'cbr')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.email_preferences": {
+ "name": "email_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "default_provider_id": {
+ "name": "default_provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_recipient_id": {
+ "name": "default_recipient_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "default_template_id": {
+ "name": "default_template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "email_preferences_user_id_users_id_fk": {
+ "name": "email_preferences_user_id_users_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_provider_id_email_providers_id_fk": {
+ "name": "email_preferences_default_provider_id_email_providers_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_providers",
+ "columnsFrom": ["default_provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_recipient_id_email_recipients_id_fk": {
+ "name": "email_preferences_default_recipient_id_email_recipients_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["default_recipient_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_template_id_email_templates_id_fk": {
+ "name": "email_preferences_default_template_id_email_templates_id_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_templates",
+ "columnsFrom": ["default_template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_preferences_default_recipient_owner_fk": {
+ "name": "email_preferences_default_recipient_owner_fk",
+ "tableFrom": "email_preferences",
+ "tableTo": "email_recipients",
+ "columnsFrom": ["default_recipient_id", "user_id"],
+ "columnsTo": ["id", "user_id"],
+ "onDelete": "no action",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "email_preferences_user_id_unique": {
+ "name": "email_preferences_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.email_send_log": {
+ "name": "email_send_log",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "provider_id": {
+ "name": "provider_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "template_id": {
+ "name": "template_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "to_email": {
+ "name": "to_email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "to_name": {
+ "name": "to_name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "subject": {
+ "name": "subject",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "status": {
+ "name": "status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'pending'"
+ },
+ "attempt_count": {
+ "name": "attempt_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 0
+ },
+ "next_retry_at": {
+ "name": "next_retry_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sent_at": {
+ "name": "sent_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "email_send_log_user_created_at_idx": {
+ "name": "email_send_log_user_created_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_created_at_idx": {
+ "name": "email_send_log_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "email_send_log_status_next_retry_idx": {
+ "name": "email_send_log_status_next_retry_idx",
+ "columns": [
+ {
+ "expression": "status",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "next_retry_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "email_send_log_user_id_users_id_fk": {
+ "name": "email_send_log_user_id_users_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "email_send_log_book_id_books_id_fk": {
+ "name": "email_send_log_book_id_books_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_book_file_id_book_files_id_fk": {
+ "name": "email_send_log_book_file_id_book_files_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_provider_id_email_providers_id_fk": {
+ "name": "email_send_log_provider_id_email_providers_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "email_providers",
+ "columnsFrom": ["provider_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "email_send_log_template_id_email_templates_id_fk": {
+ "name": "email_send_log_template_id_email_templates_id_fk",
+ "tableFrom": "email_send_log",
+ "tableTo": "email_templates",
+ "columnsFrom": ["template_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "email_send_log_status_chk": {
+ "name": "email_send_log_status_chk",
+ "value": "\"email_send_log\".\"status\" in ('pending', 'sent', 'failed')"
+ },
+ "email_send_log_attempt_count_nonnegative_chk": {
+ "name": "email_send_log_attempt_count_nonnegative_chk",
+ "value": "\"email_send_log\".\"attempt_count\" >= 0"
+ },
+ "email_send_log_sent_after_created_chk": {
+ "name": "email_send_log_sent_after_created_chk",
+ "value": "\"email_send_log\".\"sent_at\" is null or \"email_send_log\".\"sent_at\" >= \"email_send_log\".\"created_at\""
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.notifications": {
+ "name": "notifications",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "message": {
+ "name": "message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "action_url": {
+ "name": "action_url",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "meta": {
+ "name": "meta",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "read": {
+ "name": "read",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "notifications_user_id_idx": {
+ "name": "notifications_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_user_unread_idx": {
+ "name": "notifications_user_unread_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "read",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "notifications_created_at_idx": {
+ "name": "notifications_created_at_idx",
+ "columns": [
+ {
+ "expression": "created_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "notifications_user_id_users_id_fk": {
+ "name": "notifications_user_id_users_id_fk",
+ "tableFrom": "notifications",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dismissed_duplicate_pairs": {
+ "name": "dismissed_duplicate_pairs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_a": {
+ "name": "entity_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_b": {
+ "name": "entity_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_by": {
+ "name": "dismissed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "dismissed_duplicate_pairs_entity_a_idx": {
+ "name": "dismissed_duplicate_pairs_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "dismissed_duplicate_pairs_entity_b_idx": {
+ "name": "dismissed_duplicate_pairs_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "dismissed_duplicate_pairs_dismissed_by_users_id_fk": {
+ "name": "dismissed_duplicate_pairs_dismissed_by_users_id_fk",
+ "tableFrom": "dismissed_duplicate_pairs",
+ "tableTo": "users",
+ "columnsFrom": ["dismissed_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "dismissed_duplicate_pairs_unique": {
+ "name": "dismissed_duplicate_pairs_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "entity_id_a", "entity_id_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.dismissed_inline_duplicate_pairs": {
+ "name": "dismissed_inline_duplicate_pairs",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value_a": {
+ "name": "value_a",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "value_b": {
+ "name": "value_b",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "dismissed_by": {
+ "name": "dismissed_by",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "dismissed_at": {
+ "name": "dismissed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "dismissed_inline_pairs_entity_a_idx": {
+ "name": "dismissed_inline_pairs_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "dismissed_inline_pairs_entity_b_idx": {
+ "name": "dismissed_inline_pairs_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "value_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "dismissed_inline_duplicate_pairs_dismissed_by_users_id_fk": {
+ "name": "dismissed_inline_duplicate_pairs_dismissed_by_users_id_fk",
+ "tableFrom": "dismissed_inline_duplicate_pairs",
+ "tableTo": "users",
+ "columnsFrom": ["dismissed_by"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "dismissed_inline_duplicate_pairs_unique": {
+ "name": "dismissed_inline_duplicate_pairs_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "value_a", "value_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entity_duplicate_candidates": {
+ "name": "entity_duplicate_candidates",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_a": {
+ "name": "entity_id_a",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "entity_id_b": {
+ "name": "entity_id_b",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "sim_score": {
+ "name": "sim_score",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "computed_at": {
+ "name": "computed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "entity_dup_candidates_type_sim_idx": {
+ "name": "entity_dup_candidates_type_sim_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sim_score",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_dup_candidates_entity_a_idx": {
+ "name": "entity_dup_candidates_entity_a_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_a",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "entity_dup_candidates_entity_b_idx": {
+ "name": "entity_dup_candidates_entity_b_idx",
+ "columns": [
+ {
+ "expression": "entity_type",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "entity_id_b",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "entity_duplicate_candidates_unique": {
+ "name": "entity_duplicate_candidates_unique",
+ "nullsNotDistinct": false,
+ "columns": ["entity_type", "entity_id_a", "entity_id_b"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.entity_duplicate_scan_status": {
+ "name": "entity_duplicate_scan_status",
+ "schema": "",
+ "columns": {
+ "entity_type": {
+ "name": "entity_type",
+ "type": "varchar(50)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "is_computing": {
+ "name": "is_computing",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "computed_at": {
+ "name": "computed_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_pairs": {
+ "name": "total_pairs",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "threshold": {
+ "name": "threshold",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "processed_count": {
+ "name": "processed_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "total_count": {
+ "name": "total_count",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "error_message": {
+ "name": "error_message",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_fonts": {
+ "name": "user_fonts",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "family_name": {
+ "name": "family_name",
+ "type": "varchar(200)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "original_file_name": {
+ "name": "original_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "stored_file_name": {
+ "name": "stored_file_name",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "format": {
+ "name": "format",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "weight": {
+ "name": "weight",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 400
+ },
+ "style": {
+ "name": "style",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'normal'"
+ },
+ "file_size": {
+ "name": "file_size",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "uf_user_hash_uidx": {
+ "name": "uf_user_hash_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "uf_user_family_weight_style_uidx": {
+ "name": "uf_user_family_weight_style_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "family_name",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "weight",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "style",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_fonts_user_id_users_id_fk": {
+ "name": "user_fonts_user_id_users_id_fk",
+ "tableFrom": "user_fonts",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "user_fonts_format_chk": {
+ "name": "user_fonts_format_chk",
+ "value": "\"user_fonts\".\"format\" in ('ttf', 'otf', 'woff', 'woff2')"
+ },
+ "user_fonts_weight_chk": {
+ "name": "user_fonts_weight_chk",
+ "value": "\"user_fonts\".\"weight\" >= 100 and \"user_fonts\".\"weight\" <= 900 and \"user_fonts\".\"weight\" % 100 = 0"
+ },
+ "user_fonts_style_chk": {
+ "name": "user_fonts_style_chk",
+ "value": "\"user_fonts\".\"style\" in ('normal', 'italic')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.book_file_chapters": {
+ "name": "book_file_chapters",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "chapter_index": {
+ "name": "chapter_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "title": {
+ "name": "title",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "href": {
+ "name": "href",
+ "type": "varchar(1000)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "spine_index": {
+ "name": "spine_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "book_file_chapters_book_file_id_chapter_index_uidx": {
+ "name": "book_file_chapters_book_file_id_chapter_index_uidx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "chapter_index",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_file_chapters_book_file_id_idx": {
+ "name": "book_file_chapters_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_file_chapters_book_file_id_book_files_id_fk": {
+ "name": "book_file_chapters_book_file_id_book_files_id_fk",
+ "tableFrom": "book_file_chapters",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.book_file_hash_history": {
+ "name": "book_file_hash_history",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "file_hash": {
+ "name": "file_hash",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "reason": {
+ "name": "reason",
+ "type": "varchar(30)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "book_file_hash_history_book_file_id_file_hash_idx": {
+ "name": "book_file_hash_history_book_file_id_file_hash_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "book_file_hash_history_file_hash_idx": {
+ "name": "book_file_hash_history_file_hash_idx",
+ "columns": [
+ {
+ "expression": "file_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "book_file_hash_history_book_file_id_book_files_id_fk": {
+ "name": "book_file_hash_history_book_file_id_book_files_id_fk",
+ "tableFrom": "book_file_hash_history",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "book_file_hash_history_reason_chk": {
+ "name": "book_file_hash_history_reason_chk",
+ "value": "\"book_file_hash_history\".\"reason\" in ('file_write', 'external_change', 'rescan')"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_device_progress": {
+ "name": "koreader_device_progress",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "book_file_id": {
+ "name": "book_file_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "device": {
+ "name": "device",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "'KOReader'"
+ },
+ "device_id": {
+ "name": "device_id",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "percentage": {
+ "name": "percentage",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "progress": {
+ "name": "progress",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "chapter_index": {
+ "name": "chapter_index",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_timestamp": {
+ "name": "sync_timestamp",
+ "type": "bigint",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "orphaned": {
+ "name": "orphaned",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "orphaned_hash": {
+ "name": "orphaned_hash",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_device_progress_book_user_device_uidx": {
+ "name": "koreader_device_progress_book_user_device_uidx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "device_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "where": "\"koreader_device_progress\".\"orphaned\" = false",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_orphaned_hash_idx": {
+ "name": "koreader_device_progress_orphaned_hash_idx",
+ "columns": [
+ {
+ "expression": "orphaned_hash",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"koreader_device_progress\".\"orphaned\" = true",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_user_updated_at_idx": {
+ "name": "koreader_device_progress_user_updated_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "updated_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_device_progress_book_file_id_idx": {
+ "name": "koreader_device_progress_book_file_id_idx",
+ "columns": [
+ {
+ "expression": "book_file_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "where": "\"koreader_device_progress\".\"book_file_id\" is not null",
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_device_progress_book_file_id_book_files_id_fk": {
+ "name": "koreader_device_progress_book_file_id_book_files_id_fk",
+ "tableFrom": "koreader_device_progress",
+ "tableTo": "book_files",
+ "columnsFrom": ["book_file_id"],
+ "columnsTo": ["id"],
+ "onDelete": "set null",
+ "onUpdate": "no action"
+ },
+ "koreader_device_progress_user_id_users_id_fk": {
+ "name": "koreader_device_progress_user_id_users_id_fk",
+ "tableFrom": "koreader_device_progress",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "koreader_device_progress_percentage_range_chk": {
+ "name": "koreader_device_progress_percentage_range_chk",
+ "value": "\"koreader_device_progress\".\"percentage\" is null or (\"koreader_device_progress\".\"percentage\" >= 0 and \"koreader_device_progress\".\"percentage\" <= 1)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.koreader_users": {
+ "name": "koreader_users",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "username": {
+ "name": "username",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_hash": {
+ "name": "password_hash",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "password_md5": {
+ "name": "password_md5",
+ "type": "varchar(32)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_enabled": {
+ "name": "sync_enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "koreader_users_user_id_uidx": {
+ "name": "koreader_users_user_id_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "koreader_users_username_uidx": {
+ "name": "koreader_users_username_uidx",
+ "columns": [
+ {
+ "expression": "username",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "koreader_users_user_id_users_id_fk": {
+ "name": "koreader_users_user_id_users_id_fk",
+ "tableFrom": "koreader_users",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.achievements": {
+ "name": "achievements",
+ "schema": "",
+ "columns": {
+ "key": {
+ "name": "key",
+ "type": "varchar(64)",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "group_key": {
+ "name": "group_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "tier": {
+ "name": "tier",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "category": {
+ "name": "category",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(100)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "description": {
+ "name": "description",
+ "type": "varchar(500)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "icon_name": {
+ "name": "icon_name",
+ "type": "varchar(50)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "rarity": {
+ "name": "rarity",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "threshold": {
+ "name": "threshold",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hidden": {
+ "name": "hidden",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": false
+ },
+ "sort_order": {
+ "name": "sort_order",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ }
+ },
+ "indexes": {
+ "achievements_category_sort_idx": {
+ "name": "achievements_category_sort_idx",
+ "columns": [
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "sort_order",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "achievements_group_key_idx": {
+ "name": "achievements_group_key_idx",
+ "columns": [
+ {
+ "expression": "group_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {},
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {
+ "achievements_category_chk": {
+ "name": "achievements_category_chk",
+ "value": "\"achievements\".\"category\" in ('reading', 'library', 'exploration', 'dedication')"
+ },
+ "achievements_rarity_chk": {
+ "name": "achievements_rarity_chk",
+ "value": "\"achievements\".\"rarity\" in ('common', 'rare', 'epic', 'legendary')"
+ },
+ "achievements_tier_chk": {
+ "name": "achievements_tier_chk",
+ "value": "\"achievements\".\"tier\" is null or (\"achievements\".\"tier\" >= 1 and \"achievements\".\"tier\" <= 4)"
+ }
+ },
+ "isRLSEnabled": false
+ },
+ "public.user_achievements": {
+ "name": "user_achievements",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "achievement_key": {
+ "name": "achievement_key",
+ "type": "varchar(64)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "awarded_at": {
+ "name": "awarded_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "context_json": {
+ "name": "context_json",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": false
+ }
+ },
+ "indexes": {
+ "user_achievements_user_key_uidx": {
+ "name": "user_achievements_user_key_uidx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "achievement_key",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_user_id_idx": {
+ "name": "user_achievements_user_id_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ },
+ "user_achievements_user_awarded_at_idx": {
+ "name": "user_achievements_user_awarded_at_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "awarded_at",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": false,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_achievements_user_id_users_id_fk": {
+ "name": "user_achievements_user_id_users_id_fk",
+ "tableFrom": "user_achievements",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "user_achievements_achievement_key_achievements_key_fk": {
+ "name": "user_achievements_achievement_key_achievements_key_fk",
+ "tableFrom": "user_achievements",
+ "tableTo": "achievements",
+ "columnsFrom": ["achievement_key"],
+ "columnsTo": ["key"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.hardcover_book_state": {
+ "name": "hardcover_book_state",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "book_id": {
+ "name": "book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "hardcover_book_id": {
+ "name": "hardcover_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_edition_id": {
+ "name": "hardcover_edition_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_user_book_id": {
+ "name": "hardcover_user_book_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "hardcover_read_id": {
+ "name": "hardcover_read_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_method": {
+ "name": "match_method",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "match_error": {
+ "name": "match_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_status": {
+ "name": "last_synced_status",
+ "type": "varchar(20)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_progress": {
+ "name": "last_synced_progress",
+ "type": "real",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_rating": {
+ "name": "last_synced_rating",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_started_at": {
+ "name": "last_synced_started_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "last_synced_finished_at": {
+ "name": "last_synced_finished_at",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "sync_error": {
+ "name": "sync_error",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "hardcover_book_state_user_id_users_id_fk": {
+ "name": "hardcover_book_state_user_id_users_id_fk",
+ "tableFrom": "hardcover_book_state",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ },
+ "hardcover_book_state_book_id_books_id_fk": {
+ "name": "hardcover_book_state_book_id_books_id_fk",
+ "tableFrom": "hardcover_book_state",
+ "tableTo": "books",
+ "columnsFrom": ["book_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "hardcover_book_state_user_book_uidx": {
+ "name": "hardcover_book_state_user_book_uidx",
+ "nullsNotDistinct": false,
+ "columns": ["user_id", "book_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.hardcover_user_settings": {
+ "name": "hardcover_user_settings",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "api_token": {
+ "name": "api_token",
+ "type": "varchar(2048)",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "enabled": {
+ "name": "enabled",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_status_change": {
+ "name": "auto_sync_on_status_change",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_progress_update": {
+ "name": "auto_sync_on_progress_update",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "auto_sync_on_rating_change": {
+ "name": "auto_sync_on_rating_change",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "default": true
+ },
+ "privacy_setting_id": {
+ "name": "privacy_setting_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "default": 3
+ },
+ "last_synced_at": {
+ "name": "last_synced_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": false
+ },
+ "created_at": {
+ "name": "created_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {
+ "hardcover_user_settings_user_id_users_id_fk": {
+ "name": "hardcover_user_settings_user_id_users_id_fk",
+ "tableFrom": "hardcover_user_settings",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {
+ "hardcover_user_settings_user_id_unique": {
+ "name": "hardcover_user_settings_user_id_unique",
+ "nullsNotDistinct": false,
+ "columns": ["user_id"]
+ }
+ },
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ },
+ "public.user_preferences": {
+ "name": "user_preferences",
+ "schema": "",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": true,
+ "notNull": true
+ },
+ "user_id": {
+ "name": "user_id",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "category": {
+ "name": "category",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "data": {
+ "name": "data",
+ "type": "jsonb",
+ "primaryKey": false,
+ "notNull": true
+ },
+ "updated_at": {
+ "name": "updated_at",
+ "type": "timestamp with time zone",
+ "primaryKey": false,
+ "notNull": true,
+ "default": "now()"
+ }
+ },
+ "indexes": {
+ "up_user_category_idx": {
+ "name": "up_user_category_idx",
+ "columns": [
+ {
+ "expression": "user_id",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ },
+ {
+ "expression": "category",
+ "isExpression": false,
+ "asc": true,
+ "nulls": "last"
+ }
+ ],
+ "isUnique": true,
+ "concurrently": false,
+ "method": "btree",
+ "with": {}
+ }
+ },
+ "foreignKeys": {
+ "user_preferences_user_id_users_id_fk": {
+ "name": "user_preferences_user_id_users_id_fk",
+ "tableFrom": "user_preferences",
+ "tableTo": "users",
+ "columnsFrom": ["user_id"],
+ "columnsTo": ["id"],
+ "onDelete": "cascade",
+ "onUpdate": "no action"
+ }
+ },
+ "compositePrimaryKeys": {},
+ "uniqueConstraints": {},
+ "policies": {},
+ "checkConstraints": {},
+ "isRLSEnabled": false
+ }
+ },
+ "enums": {
+ "public.content_filter_type": {
+ "name": "content_filter_type",
+ "schema": "public",
+ "values": ["include", "exclude"]
+ },
+ "public.library_access_level": {
+ "name": "library_access_level",
+ "schema": "public",
+ "values": ["viewer", "editor", "owner"]
+ },
+ "public.user_avatar_source": {
+ "name": "user_avatar_source",
+ "schema": "public",
+ "values": ["none", "external", "uploaded"]
+ },
+ "public.opds_sort_order": {
+ "name": "opds_sort_order",
+ "schema": "public",
+ "values": ["recent", "title_asc", "title_desc", "author_asc", "author_desc", "series_asc", "series_desc"]
+ }
+ },
+ "schemas": {},
+ "sequences": {},
+ "roles": {},
+ "policies": {},
+ "views": {},
+ "_meta": {
+ "columns": {},
+ "schemas": {},
+ "tables": {}
+ }
+}
diff --git a/server/src/db/migrations/meta/_journal.json b/server/src/db/migrations/meta/_journal.json
index b6cb14a01..1dc0368ec 100644
--- a/server/src/db/migrations/meta/_journal.json
+++ b/server/src/db/migrations/meta/_journal.json
@@ -134,6 +134,13 @@
"when": 1781214276180,
"tag": "0018_author_sort_key_and_listing_indexes",
"breakpoints": true
+ },
+ {
+ "idx": 19,
+ "version": "7",
+ "when": 1781330635947,
+ "tag": "0019_add_lubimyczytac_provider_id",
+ "breakpoints": true
}
]
}
diff --git a/server/src/db/schema/metadata.ts b/server/src/db/schema/metadata.ts
index 8416e4f37..870968afe 100644
--- a/server/src/db/schema/metadata.ts
+++ b/server/src/db/schema/metadata.ts
@@ -68,6 +68,7 @@ export const bookMetadata = pgTable(
audibleId: varchar('audible_id', { length: 20 }),
comicvineId: varchar('comicvine_id', { length: 50 }),
ranobedbId: varchar('ranobedb_id', { length: 50 }),
+ lubimyczytacId: text('lubimyczytac_id'),
chapters: jsonb('chapters'),
lockedFields: text('locked_fields')
.array()
diff --git a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
index 07a19add5..0724453c3 100644
--- a/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
+++ b/server/src/modules/book-metadata-fetch/book-metadata-fetch-orchestrator.service.ts
@@ -291,6 +291,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
if (filteredProviderIds[MetadataProviderKey.KOBO]) scalarFields.koboId = filteredProviderIds[MetadataProviderKey.KOBO];
if (filteredProviderIds[MetadataProviderKey.COMICVINE]) scalarFields.comicvineId = filteredProviderIds[MetadataProviderKey.COMICVINE];
if (filteredProviderIds[MetadataProviderKey.RANOBEDB]) scalarFields.ranobedbId = filteredProviderIds[MetadataProviderKey.RANOBEDB];
+ if (filteredProviderIds[MetadataProviderKey.LUBIMYCZYTAC]) scalarFields.lubimyczytacId = filteredProviderIds[MetadataProviderKey.LUBIMYCZYTAC];
const duration = this.asNullableNumber(filteredResolved.duration);
if (duration !== undefined) scalarFields.durationSeconds = duration;
@@ -380,6 +381,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
koboId?: string | null;
comicvineId?: string | null;
ranobedbId?: string | null;
+ lubimyczytacId?: string | null;
}): Partial> {
const ids: Partial> = {};
if (meta.googleBooksId) ids[MetadataProviderKey.GOOGLE] = meta.googleBooksId;
@@ -392,6 +394,7 @@ export class BookMetadataFetchOrchestratorService implements OnApplicationBootst
if (meta.koboId) ids[MetadataProviderKey.KOBO] = meta.koboId;
if (meta.comicvineId) ids[MetadataProviderKey.COMICVINE] = meta.comicvineId;
if (meta.ranobedbId) ids[MetadataProviderKey.RANOBEDB] = meta.ranobedbId;
+ if (meta.lubimyczytacId) ids[MetadataProviderKey.LUBIMYCZYTAC] = meta.lubimyczytacId;
return ids;
}
diff --git a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
index 9dcc2f7d4..1e03e0844 100644
--- a/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
+++ b/server/src/modules/book-metadata-lock/book-metadata-lock.service.ts
@@ -19,6 +19,7 @@ const PROVIDER_ID_LOCK_FIELDS_BY_PROVIDER: Partial = {
@@ -241,6 +242,7 @@ export class BookMetadataLockService {
this.copyUpdateField(filteredDto, dto, 'koboId', 'koboId', lockedSet, skippedFields);
this.copyUpdateField(filteredDto, dto, 'comicvineId', 'comicvineId', lockedSet, skippedFields);
this.copyUpdateField(filteredDto, dto, 'ranobedbId', 'ranobedbId', lockedSet, skippedFields);
+ this.copyUpdateField(filteredDto, dto, 'lubimyczytacId', 'lubimyczytacId', lockedSet, skippedFields);
if (dto.audioMetadata) {
const filteredAudioMetadata: NonNullable = {};
@@ -366,6 +368,7 @@ export class BookMetadataLockService {
this.addFieldIfPresent(fields, dto, 'koboId', 'koboId');
this.addFieldIfPresent(fields, dto, 'comicvineId', 'comicvineId');
this.addFieldIfPresent(fields, dto, 'ranobedbId', 'ranobedbId');
+ this.addFieldIfPresent(fields, dto, 'lubimyczytacId', 'lubimyczytacId');
if (dto.audioMetadata) {
this.addFieldIfPresent(fields, dto.audioMetadata, 'narrators', 'narrators');
diff --git a/server/src/modules/book/book.service.ts b/server/src/modules/book/book.service.ts
index b9b49d7db..a7caf804c 100644
--- a/server/src/modules/book/book.service.ts
+++ b/server/src/modules/book/book.service.ts
@@ -273,6 +273,7 @@ export class BookService {
koboId?: string | null;
comicvineId?: string | null;
ranobedbId?: string | null;
+ lubimyczytacId?: string | null;
}): Partial> {
const providerIds: Partial> = {};
if (meta.googleBooksId) providerIds[MetadataProviderKey.GOOGLE] = meta.googleBooksId;
@@ -285,6 +286,7 @@ export class BookService {
if (meta.koboId) providerIds[MetadataProviderKey.KOBO] = meta.koboId;
if (meta.comicvineId) providerIds[MetadataProviderKey.COMICVINE] = meta.comicvineId;
if (meta.ranobedbId) providerIds[MetadataProviderKey.RANOBEDB] = meta.ranobedbId;
+ if (meta.lubimyczytacId) providerIds[MetadataProviderKey.LUBIMYCZYTAC] = meta.lubimyczytacId;
return providerIds;
}
@@ -301,6 +303,7 @@ export class BookService {
| 'koboId'
| 'comicvineId'
| 'ranobedbId'
+ | 'lubimyczytacId'
>,
providerIds: Partial>,
): void {
@@ -314,6 +317,7 @@ export class BookService {
if (providerIds[MetadataProviderKey.KOBO]) dto.koboId = providerIds[MetadataProviderKey.KOBO];
if (providerIds[MetadataProviderKey.COMICVINE]) dto.comicvineId = providerIds[MetadataProviderKey.COMICVINE];
if (providerIds[MetadataProviderKey.RANOBEDB]) dto.ranobedbId = providerIds[MetadataProviderKey.RANOBEDB];
+ if (providerIds[MetadataProviderKey.LUBIMYCZYTAC]) dto.lubimyczytacId = providerIds[MetadataProviderKey.LUBIMYCZYTAC];
}
private buildMetadataRefreshPreview(
@@ -1430,6 +1434,7 @@ export class BookService {
if (dto.koboId !== undefined) scalarFields.koboId = dto.koboId ?? null;
if (dto.comicvineId !== undefined) scalarFields.comicvineId = dto.comicvineId ?? null;
if (dto.ranobedbId !== undefined) scalarFields.ranobedbId = dto.ranobedbId ?? null;
+ if (dto.lubimyczytacId !== undefined) scalarFields.lubimyczytacId = dto.lubimyczytacId ?? null;
if (dto.rating !== undefined) scalarFields.rating = dto.rating ?? null;
if (dto.audioMetadata) {
if (dto.audioMetadata.durationSeconds !== undefined) scalarFields.durationSeconds = dto.audioMetadata.durationSeconds ?? null;
@@ -2648,6 +2653,7 @@ export class BookService {
[MetadataProviderKey.KOBO]: meta?.koboId ?? null,
[MetadataProviderKey.COMICVINE]: meta?.comicvineId ?? null,
[MetadataProviderKey.RANOBEDB]: meta?.ranobedbId ?? null,
+ [MetadataProviderKey.LUBIMYCZYTAC]: meta?.lubimyczytacId ?? null,
},
authors: authorRows,
genres: genreRows.map((g) => g.name),
@@ -2755,6 +2761,7 @@ export class BookService {
itunesId: parsed.itunesId,
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
+ lubimyczytacId: parsed.lubimyczytacId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: parsed.genres.length > 0 ? parsed.genres : undefined,
};
@@ -2785,6 +2792,7 @@ export class BookService {
itunesId: parsed.itunesId,
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
+ lubimyczytacId: parsed.lubimyczytacId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: parsed.genres.length > 0 ? parsed.genres : undefined,
};
@@ -2833,6 +2841,7 @@ export class BookService {
itunesId: parsed.itunesId,
koboId: parsed.koboId,
ranobedbId: parsed.ranobedbId,
+ lubimyczytacId: parsed.lubimyczytacId,
authors: parsed.authors.length > 0 ? parsed.authors.map((a) => a.name) : undefined,
genres: cbzGenres.length > 0 ? cbzGenres : undefined,
comicMetadata: parsed.comicMetadata ?? undefined,
diff --git a/server/src/modules/book/dto/update-book-metadata.dto.ts b/server/src/modules/book/dto/update-book-metadata.dto.ts
index e1b4af7ec..fcfb9e4c7 100644
--- a/server/src/modules/book/dto/update-book-metadata.dto.ts
+++ b/server/src/modules/book/dto/update-book-metadata.dto.ts
@@ -55,5 +55,6 @@ export class UpdateBookMetadataDto {
@IsOptional() @ValidateNested() @Type(() => AudioMetadataDto) audioMetadata?: AudioMetadataDto;
@IsOptional() @IsString() @MaxLength(50) comicvineId?: string | null;
@IsOptional() @IsString() @MaxLength(50) ranobedbId?: string | null;
+ @IsOptional() @IsString() @MaxLength(512) lubimyczytacId?: string | null;
@IsOptional() @ValidateNested() @Type(() => ComicMetadataDto) comicMetadata?: ComicMetadataDto;
}
diff --git a/server/src/modules/cover/providers/itunes-cover-provider.test.ts b/server/src/modules/cover/providers/itunes-cover-provider.test.ts
index 9f3f12697..84b42839c 100644
--- a/server/src/modules/cover/providers/itunes-cover-provider.test.ts
+++ b/server/src/modules/cover/providers/itunes-cover-provider.test.ts
@@ -15,6 +15,7 @@ const baseConfig: ProviderConfigurations = {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
function mockJsonResponse(body: unknown, status = 200): Response {
diff --git a/server/src/modules/file-write/file-write.constants.ts b/server/src/modules/file-write/file-write.constants.ts
index 34f41d7ad..3c5bcb934 100644
--- a/server/src/modules/file-write/file-write.constants.ts
+++ b/server/src/modules/file-write/file-write.constants.ts
@@ -38,6 +38,7 @@ export const COMIC_INFO_MANAGED_NOTES_KEYS = [
'openLibraryId',
'ranobedbId',
'koboId',
+ 'lubimyczytacId',
] as const satisfies readonly BookWritePayloadKey[];
type ComicInfoProviderKey = (typeof COMIC_INFO_PROVIDER_ID_KEYS)[number];
@@ -59,5 +60,6 @@ export const EPUB_PROVIDER_IDENTIFIER_SCHEMES = {
openLibraryId: 'OPENLIBRARY',
ranobedbId: 'RANOBEDB',
koboId: 'KOBO',
+ lubimyczytacId: 'LUBIMYCZYTAC',
itunesId: 'ITUNES',
} as const satisfies Partial>;
diff --git a/server/src/modules/file-write/file-write.repository.ts b/server/src/modules/file-write/file-write.repository.ts
index 4fab2ce23..281b4a76f 100644
--- a/server/src/modules/file-write/file-write.repository.ts
+++ b/server/src/modules/file-write/file-write.repository.ts
@@ -185,6 +185,7 @@ export class FileWriteRepository {
openLibraryId: meta.openLibraryId,
ranobedbId: meta.ranobedbId,
koboId: meta.koboId,
+ lubimyczytacId: meta.lubimyczytacId,
itunesId: meta.itunesId,
audibleId: meta.audibleId,
rating: meta.rating,
diff --git a/server/src/modules/file-write/interfaces/book-write-payload.interface.ts b/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
index c7d32504d..b0f3c01c0 100644
--- a/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
+++ b/server/src/modules/file-write/interfaces/book-write-payload.interface.ts
@@ -23,6 +23,7 @@ export interface BookWritePayload {
openLibraryId?: string | null;
ranobedbId?: string | null;
koboId?: string | null;
+ lubimyczytacId?: string | null;
comicIssueNumber?: string | null;
comicVolumeName?: string | null;
comicPencillers?: string[];
diff --git a/server/src/modules/metadata-fetch/metadata-fetch-pipeline.test.ts b/server/src/modules/metadata-fetch/metadata-fetch-pipeline.test.ts
index 2d2cdb176..cab9afde3 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch-pipeline.test.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch-pipeline.test.ts
@@ -130,6 +130,7 @@ describe('MetadataFetchPipeline', () => {
google: { enabled: false, apiKey: '' },
openLibrary: { enabled: false },
kobo: { enabled: true, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
}),
);
preferencesService.getGlobal.mockResolvedValue(global);
@@ -575,5 +576,6 @@ function makeProviderConfig(overrides: Partial = {}): Pr
comicvine: { enabled: true, apiKey: 'comicvine-key', ...overrides.comicvine },
ranobedb: { enabled: true, ...overrides.ranobedb },
kobo: { enabled: true, country: 'us', language: 'en', ...overrides.kobo },
+ lubimyczytac: { enabled: false, ...overrides.lubimyczytac },
};
}
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.controller.test.ts b/server/src/modules/metadata-fetch/metadata-fetch.controller.test.ts
index c69a0de9e..b55e50e73 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.controller.test.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.controller.test.ts
@@ -171,6 +171,7 @@ describe('MetadataFetchController', () => {
audible: { enabled: false, domain: 'com' },
audnexus: { enabled: false },
kobo: { enabled: true, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
}),
);
service.search.mockReturnValue(of({ provider: MetadataProviderKey.KOBO, providerId: 'dune-1', title: 'Dune' }));
@@ -313,5 +314,6 @@ function makeProviderConfig(overrides: Partial = {}): Pr
comicvine: { enabled: false, apiKey: '', ...overrides.comicvine },
ranobedb: { enabled: false, ...overrides.ranobedb },
kobo: { enabled: true, country: 'us', language: 'en', ...overrides.kobo },
+ lubimyczytac: { enabled: false, ...overrides.lubimyczytac },
};
}
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts b/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts
index 77bc186dc..116e69dce 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.module.test.ts
@@ -19,6 +19,7 @@ import { HardcoverProvider } from './providers/hardcover/hardcover.provider';
import { ComicVineProvider } from './providers/comicvine/comicvine.provider';
import { KoboProvider } from './providers/kobo/kobo.provider';
import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider';
+import { LubimyczytacProvider } from './providers/lubimyczytac/lubimyczytac.provider';
describe('MetadataFetchModule', () => {
it('registers provider classes and factory wiring for METADATA_PROVIDERS', () => {
@@ -37,6 +38,7 @@ describe('MetadataFetchModule', () => {
ComicVineProvider,
RanobeDbProvider,
KoboProvider,
+ LubimyczytacProvider,
ProviderRegistry,
MetadataFetchRepository,
MetadataFetchService,
@@ -62,6 +64,7 @@ describe('MetadataFetchModule', () => {
ComicVineProvider,
RanobeDbProvider,
KoboProvider,
+ LubimyczytacProvider,
]);
const google = { key: 'google' };
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.module.ts b/server/src/modules/metadata-fetch/metadata-fetch.module.ts
index 2c4184136..9d0e2dc99 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.module.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.module.ts
@@ -23,6 +23,7 @@ import { ComicVineProvider } from './providers/comicvine/comicvine.provider';
import { KoboProvider } from './providers/kobo/kobo.provider';
import { RanobeDbClient } from './providers/ranobedb/ranobedb.client';
import { RanobeDbProvider } from './providers/ranobedb/ranobedb.provider';
+import { LubimyczytacProvider } from './providers/lubimyczytac/lubimyczytac.provider';
const PROVIDER_CLASSES = [
GoogleProvider,
@@ -36,6 +37,7 @@ const PROVIDER_CLASSES = [
ComicVineProvider,
RanobeDbProvider,
KoboProvider,
+ LubimyczytacProvider,
];
@Module({
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
index bd5535f0c..ea9699e70 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.repository.ts
@@ -20,6 +20,7 @@ export interface StoredProviderIdsRow {
koboId: string | null;
comicvineId: string | null;
ranobedbId: string | null;
+ lubimyczytacId: string | null;
}
@Injectable()
@@ -40,6 +41,7 @@ export class MetadataFetchRepository {
koboId: bookMetadata.koboId,
comicvineId: bookMetadata.comicvineId,
ranobedbId: bookMetadata.ranobedbId,
+ lubimyczytacId: bookMetadata.lubimyczytacId,
})
.from(books)
.leftJoin(bookMetadata, eq(bookMetadata.bookId, books.id))
diff --git a/server/src/modules/metadata-fetch/metadata-fetch.service.ts b/server/src/modules/metadata-fetch/metadata-fetch.service.ts
index 3eac505d5..7319787c3 100644
--- a/server/src/modules/metadata-fetch/metadata-fetch.service.ts
+++ b/server/src/modules/metadata-fetch/metadata-fetch.service.ts
@@ -72,6 +72,7 @@ export class MetadataFetchService {
[MetadataProviderKey.KOBO]: row.koboId ?? undefined,
[MetadataProviderKey.COMICVINE]: row.comicvineId ?? undefined,
[MetadataProviderKey.RANOBEDB]: row.ranobedbId ?? undefined,
+ [MetadataProviderKey.LUBIMYCZYTAC]: row.lubimyczytacId ?? undefined,
};
}
diff --git a/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts b/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts
index 8ac8c57da..3e21a6ff7 100644
--- a/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/amazon/amazon.provider.test.ts
@@ -21,6 +21,7 @@ describe('AmazonProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(async () => {
diff --git a/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts b/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts
index 601fef772..316547fc6 100644
--- a/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/audible/audible.provider.test.ts
@@ -39,6 +39,7 @@ const baseConfig: ProviderConfigurations = {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
function makeProvider(config: ProviderConfigurations = baseConfig): {
diff --git a/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts b/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts
index 0b0fa535f..61e622fbe 100644
--- a/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/audnexus/audnexus.provider.test.ts
@@ -30,6 +30,7 @@ describe('AudnexusProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(async () => {
diff --git a/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts b/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts
index 4c3008cf8..059933f24 100644
--- a/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/comicvine/comicvine.provider.test.ts
@@ -20,6 +20,7 @@ const mockConfig: ProviderConfigurations = {
comicvine: { enabled: true, apiKey: 'test-key' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
const mockVolume = {
diff --git a/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts b/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts
index c8de28f77..0753238aa 100644
--- a/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/goodreads/goodreads.provider.test.ts
@@ -20,6 +20,7 @@ describe('GoodreadsProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
function goodreadsBookHtml(bookId: string, title: string): string {
diff --git a/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts b/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts
index 87787463e..ba5260e67 100644
--- a/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/google/google.provider.test.ts
@@ -20,6 +20,7 @@ describe('GoogleProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(async () => {
diff --git a/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts b/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts
index 494edd207..470d30b0c 100644
--- a/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/hardcover/hardcover.provider.test.ts
@@ -18,6 +18,7 @@ const mockConfig: ProviderConfigurations = {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
const mockDocument: HardcoverSearchDocument = {
diff --git a/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts b/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts
index 0794f5842..8a5bdc0bf 100644
--- a/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/itunes/itunes.provider.test.ts
@@ -20,6 +20,7 @@ describe('ITunesProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(async () => {
diff --git a/server/src/modules/metadata-fetch/providers/kobo/kobo.provider.test.ts b/server/src/modules/metadata-fetch/providers/kobo/kobo.provider.test.ts
index bd4c1debe..7de6b61b8 100644
--- a/server/src/modules/metadata-fetch/providers/kobo/kobo.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/kobo/kobo.provider.test.ts
@@ -28,6 +28,7 @@ describe('KoboProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: true, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(async () => {
@@ -62,6 +63,7 @@ describe('KoboProvider', () => {
vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({
...mockConfig,
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
});
await expect(provider.search({ title: 'Fourth Wing' })).resolves.toEqual([]);
diff --git a/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.test.ts b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.test.ts
new file mode 100644
index 000000000..ac6661d14
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.test.ts
@@ -0,0 +1,216 @@
+import { Test, TestingModule } from '@nestjs/testing';
+import { MetadataProviderKey, ProviderConfigurations } from '@bookorbit/types';
+
+import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service';
+import { ProviderThrottleError } from '../../provider-throttle.error';
+import { LubimyczytacProvider } from './lubimyczytac.provider';
+
+const SEARCH_HTML = `
+
+`;
+
+const BOOK_HTML = `
+ Ostatnie życzenie
+
+ superNOWA
+ Język: polski
+
+
+`;
+
+describe('LubimyczytacProvider', () => {
+ let provider: LubimyczytacProvider;
+ let providerConfig: ProviderConfigService;
+
+ const mockConfig: ProviderConfigurations = {
+ google: { enabled: true, apiKey: '' },
+ amazon: { enabled: true, domain: 'amazon.com', cookie: '' },
+ goodreads: { enabled: true },
+ hardcover: { enabled: false, apiKey: '' },
+ openLibrary: { enabled: true },
+ itunes: { enabled: true, coverResolution: 'high' },
+ audible: { enabled: false, domain: 'com' },
+ audnexus: { enabled: false },
+ comicvine: { enabled: false, apiKey: '' },
+ ranobedb: { enabled: false },
+ kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: true },
+ };
+
+ beforeEach(async () => {
+ const module: TestingModule = await Test.createTestingModule({
+ providers: [
+ LubimyczytacProvider,
+ {
+ provide: ProviderConfigService,
+ useValue: { getConfig: vi.fn().mockResolvedValue(mockConfig) },
+ },
+ ],
+ }).compile();
+
+ provider = module.get(LubimyczytacProvider);
+ providerConfig = module.get(ProviderConfigService);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ function mockFetchSequence(...bodies: string[]) {
+ const fetch = vi.fn();
+ for (const body of bodies) {
+ fetch.mockResolvedValueOnce({ ok: true, status: 200, text: () => Promise.resolve(body) } as Response);
+ }
+ global.fetch = fetch;
+ return fetch;
+ }
+
+ describe('search', () => {
+ it('returns empty array when disabled', async () => {
+ vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({ ...mockConfig, lubimyczytac: { enabled: false } });
+ const result = await provider.search({ title: 'Wiedźmin' });
+ expect(result).toEqual([]);
+ });
+
+ it('searches by title then fetches book details', async () => {
+ mockFetchSequence(SEARCH_HTML, BOOK_HTML);
+
+ const result = await provider.search({ title: 'Ostatnie życzenie', author: 'Andrzej Sapkowski' });
+
+ expect(result).toHaveLength(1);
+ expect(result[0]).toMatchObject({
+ provider: MetadataProviderKey.LUBIMYCZYTAC,
+ providerId: '123456/ostatnie-zyczenie',
+ title: 'Ostatnie życzenie',
+ authors: ['Andrzej Sapkowski'],
+ publisher: 'superNOWA',
+ language: 'pl',
+ pageCount: 330,
+ publishedYear: 2014,
+ isbn13: '9788375780635',
+ sourceUrl: 'https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie',
+ });
+ });
+
+ it('returns empty array when there is no query', async () => {
+ const fetch = mockFetchSequence();
+ const result = await provider.search({});
+ expect(result).toEqual([]);
+ expect(fetch).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('lookupById', () => {
+ it('fetches a book by its provider id', async () => {
+ mockFetchSequence(BOOK_HTML);
+ const result = await provider.lookupById('123456');
+ expect(result?.providerId).toBe('123456/ostatnie-zyczenie');
+ expect(result?.title).toBe('Ostatnie życzenie');
+ });
+
+ it('returns null when disabled', async () => {
+ vi.spyOn(providerConfig, 'getConfig').mockResolvedValue({ ...mockConfig, lubimyczytac: { enabled: false } });
+ expect(await provider.lookupById('123456')).toBeNull();
+ });
+
+ it('returns null for an invalid providerId', async () => {
+ expect(await provider.lookupById('not-a-valid-id')).toBeNull();
+ });
+
+ it('returns null when book page has no title', async () => {
+ mockFetchSequence('');
+ expect(await provider.lookupById('123456')).toBeNull();
+ });
+
+ it('returns null on non-ok HTTP response', async () => {
+ global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, text: () => Promise.resolve('') } as unknown as Response);
+ expect(await provider.lookupById('123456')).toBeNull();
+ });
+
+ it('rethrows ProviderThrottleError', async () => {
+ global.fetch = vi.fn().mockResolvedValue({
+ status: 429,
+ ok: false,
+ headers: { get: () => null },
+ } as unknown as Response);
+ await expect(provider.lookupById('123456')).rejects.toBeInstanceOf(ProviderThrottleError);
+ });
+
+ it('returns null on generic fetch error', async () => {
+ global.fetch = vi.fn().mockRejectedValue(new Error('network failure'));
+ expect(await provider.lookupById('123456')).toBeNull();
+ });
+
+ it('returns null on non-Error rejection', async () => {
+ global.fetch = vi.fn().mockRejectedValue('string rejection');
+ expect(await provider.lookupById('123456')).toBeNull();
+ });
+ });
+
+ describe('search — additional branches', () => {
+ it('searches by isbn when no title is given', async () => {
+ mockFetchSequence(SEARCH_HTML, BOOK_HTML);
+ const result = await provider.search({ isbn: '9788375780635' });
+ expect(result).toHaveLength(1);
+ });
+
+ it('returns empty array on non-ok search response', async () => {
+ global.fetch = vi.fn().mockResolvedValue({ ok: false, status: 503, text: () => Promise.resolve('') } as unknown as Response);
+ expect(await provider.search({ title: 'Wiedźmin' })).toEqual([]);
+ });
+
+ it('skips book fetch when book page returns no title', async () => {
+ const SEARCH_TWO = `
+ First
+ Second
+ `;
+ mockFetchSequence(SEARCH_TWO, '', '');
+ expect(await provider.search({ title: 'Wiedźmin' })).toEqual([]);
+ });
+
+ it('omits authors and genres fields when empty', async () => {
+ const NO_AUTHORS_HTML = `
+ Test Book
+
+ `;
+ mockFetchSequence(SEARCH_HTML, NO_AUTHORS_HTML);
+ const result = await provider.search({ title: 'Test' });
+ expect(result[0]?.authors).toBeUndefined();
+ expect(result[0]?.genres).toBeUndefined();
+ });
+
+ it('breaks the fetch loop when signal is pre-aborted', async () => {
+ const SEARCH_TWO = `
+ First
+ Second
+ `;
+ const fetch = mockFetchSequence(SEARCH_TWO);
+ const result = await provider.search({ title: 'Wiedźmin', signal: AbortSignal.abort() });
+ expect(result).toEqual([]);
+ expect(fetch).toHaveBeenCalledTimes(1);
+ });
+
+ it('sleeps between multiple book fetches', async () => {
+ const SEARCH_TWO = `
+ Ostatnie życzenie
+ Miecz przeznaczenia
+ `;
+ const BOOK_HTML_2 = `
+ Miecz przeznaczenia
+
+ `;
+ mockFetchSequence(SEARCH_TWO, BOOK_HTML, BOOK_HTML_2);
+ const result = await provider.search({ title: 'Sapkowski' });
+ expect(result).toHaveLength(2);
+ expect(result[1]?.title).toBe('Miecz przeznaczenia');
+ }, 10_000);
+ });
+});
diff --git a/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts
new file mode 100644
index 000000000..d39517a07
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.provider.ts
@@ -0,0 +1,144 @@
+import { Injectable, Logger } from '@nestjs/common';
+import { MetadataCandidate, MetadataProviderKey } from '@bookorbit/types';
+
+import { sanitizeLogValue } from '../../../../common/utils/log-sanitize.utils';
+import { ProviderConfigService } from '../../../metadata-preferences/provider-config.service';
+import { fetchWithThrottle } from '../../fetch-with-throttle';
+import { ProviderThrottleError } from '../../provider-throttle.error';
+import { IdentifiableProvider } from '../metadata-provider';
+import { PROVIDER_DELAYS_MS, PROVIDER_LIMITS, PROVIDER_TIMEOUT_MS } from '../provider-constants';
+import { MetadataSearchParams } from '../metadata-search-params';
+import { buildRequestSignal, normalizeMaxCandidates, sleep } from '../provider-utils';
+import {
+ buildLubimyczytacBookUrl,
+ buildLubimyczytacSearchUrl,
+ extractLubimyczytacPath,
+ extractLubimyczytacSearchResults,
+ LubimyczytacSearchResultLink,
+ parseLubimyczytacBookPage,
+} from './lubimyczytac.scraper';
+
+const HEADERS: HeadersInit = {
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36',
+ accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
+ 'accept-language': 'pl-PL,pl;q=0.9,en;q=0.8',
+};
+
+type FetchContext = {
+ op: 'search' | 'lookup';
+ query?: string;
+ providerId?: string;
+};
+
+@Injectable()
+export class LubimyczytacProvider implements IdentifiableProvider {
+ readonly key = MetadataProviderKey.LUBIMYCZYTAC;
+ readonly label = 'LubimyCzytac';
+ readonly identifiable = true as const;
+ readonly timeoutMs = PROVIDER_TIMEOUT_MS.SCRAPE;
+
+ private readonly logger = new Logger(LubimyczytacProvider.name);
+
+ constructor(private readonly providerConfig: ProviderConfigService) {}
+
+ async search(params: MetadataSearchParams): Promise {
+ const { enabled } = await this.providerConfig.getConfig().then((c) => c.lubimyczytac);
+ if (!enabled) return [];
+
+ const maxCandidates = normalizeMaxCandidates(params.maxCandidatesPerProvider, PROVIDER_LIMITS.LUBIMYCZYTAC_MAX_RESULTS);
+ const links = await this.searchBookLinks(params, maxCandidates, params.signal);
+
+ const results: MetadataCandidate[] = [];
+ for (const link of links.slice(0, maxCandidates)) {
+ if (params.signal?.aborted) break;
+ if (results.length > 0) {
+ await sleep(PROVIDER_DELAYS_MS.LUBIMYCZYTAC_BETWEEN_REQUESTS, params.signal);
+ }
+ const candidate = await this.fetchByUrl(link.url, link.providerId, params.signal);
+ if (candidate) results.push(candidate);
+ }
+
+ return results;
+ }
+
+ async lookupById(providerId: string, signal?: AbortSignal): Promise {
+ const { enabled } = await this.providerConfig.getConfig().then((c) => c.lubimyczytac);
+ if (!enabled) return null;
+ const normalized = extractLubimyczytacPath(providerId);
+ if (!normalized) return null;
+ return this.fetchByUrl(buildLubimyczytacBookUrl(normalized), normalized, signal);
+ }
+
+ private async searchBookLinks(params: MetadataSearchParams, limit: number, signal?: AbortSignal): Promise {
+ const query = params.title?.trim() || params.isbn?.trim();
+ if (!query) return [];
+
+ const url = buildLubimyczytacSearchUrl(query, params.author);
+ const html = await this.fetchHtml(url, { op: 'search', query }, signal);
+ return html ? extractLubimyczytacSearchResults(html, limit) : [];
+ }
+
+ private async fetchByUrl(url: string, fallbackProviderId: string, signal?: AbortSignal): Promise {
+ const html = await this.fetchHtml(url, { op: 'lookup', providerId: fallbackProviderId }, signal);
+ if (!html) return null;
+
+ const data = parseLubimyczytacBookPage(html, url);
+ const providerId = data.providerId || extractLubimyczytacPath(url) || fallbackProviderId;
+ if (!data.title || !providerId) return null;
+
+ return {
+ provider: MetadataProviderKey.LUBIMYCZYTAC,
+ providerId,
+ title: data.title,
+ authors: data.authors?.length ? data.authors : undefined,
+ description: data.description,
+ publisher: data.publisher,
+ publishedYear: data.publishedYear,
+ language: data.language,
+ pageCount: data.pageCount,
+ isbn10: data.isbn10,
+ isbn13: data.isbn13,
+ seriesName: data.seriesName,
+ seriesIndex: data.seriesIndex,
+ genres: data.genres?.length ? data.genres : undefined,
+ coverUrl: data.coverUrl,
+ sourceUrl: url,
+ };
+ }
+
+ private async fetchHtml(url: string, context: FetchContext, signal?: AbortSignal): Promise {
+ const startedAt = Date.now();
+ const safeQuery = context.query ? sanitizeLogValue(context.query) : undefined;
+ const safeProviderId = context.providerId ? sanitizeLogValue(context.providerId) : undefined;
+ const subject = `${safeQuery ? ` query="${safeQuery}"` : ''}${safeProviderId ? ` providerId="${safeProviderId}"` : ''}`;
+ this.logger.log(`[lubimyczytac] [start] op=${context.op}${subject} - lubimyczytac fetch started`);
+
+ try {
+ const res = await fetchWithThrottle(url, { headers: HEADERS, signal: buildRequestSignal(PROVIDER_TIMEOUT_MS.SCRAPE, signal) });
+ if (!res.ok) {
+ this.logger.warn(
+ `[lubimyczytac] [fail] op=${context.op}${subject} status=${res.status} durationMs=${Date.now() - startedAt} errorClass=HttpError error="non-ok response" - lubimyczytac fetch failed`,
+ );
+ return null;
+ }
+ const html = await res.text();
+ this.logger.log(
+ `[lubimyczytac] [end] op=${context.op}${subject} status=${res.status} durationMs=${Date.now() - startedAt} bytes=${html.length} - lubimyczytac fetch completed`,
+ );
+ return html;
+ } catch (error) {
+ if (error instanceof ProviderThrottleError) {
+ this.logger.warn(
+ `[lubimyczytac] [fail] op=${context.op}${subject} durationMs=${Date.now() - startedAt} errorClass=ProviderThrottleError error="throttled" - lubimyczytac fetch throttled`,
+ );
+ throw error;
+ }
+ const errorClass = error instanceof Error ? error.name : 'UnknownError';
+ const message = sanitizeLogValue(error instanceof Error ? error.message : String(error));
+ this.logger.warn(
+ `[lubimyczytac] [fail] op=${context.op}${subject} durationMs=${Date.now() - startedAt} errorClass=${errorClass} error="${message}" - lubimyczytac fetch failed`,
+ );
+ return null;
+ }
+ }
+}
diff --git a/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.test.ts b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.test.ts
new file mode 100644
index 000000000..84f930f26
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.test.ts
@@ -0,0 +1,439 @@
+import {
+ buildLubimyczytacBookUrl,
+ buildLubimyczytacSearchUrl,
+ extractLubimyczytacId,
+ extractLubimyczytacPath,
+ extractLubimyczytacSearchResults,
+ parseLubimyczytacBookPage,
+} from './lubimyczytac.scraper';
+
+const BOOK_HTML = `
+ Ostatnie życzenie
+
+ superNOWA
+
+ Język: polski
+
+ Opis książki o wiedźminie Geralcie.
+
+ Fantasy, fantastyka
+ Cykl: Wiedźmin (tom 1)
+
+`;
+
+describe('LubimyczytacScraper', () => {
+ describe('buildLubimyczytacSearchUrl', () => {
+ it('encodes phrase and author', () => {
+ const url = buildLubimyczytacSearchUrl('Wiedźmin', 'Andrzej Sapkowski');
+ expect(url).toContain('https://lubimyczytac.pl/szukaj/ksiazki');
+ expect(url).toContain('phrase=Wied%C5%BAmin');
+ expect(url).toContain('author=Andrzej+Sapkowski');
+ });
+
+ it('omits author when not provided', () => {
+ expect(buildLubimyczytacSearchUrl('Lalka')).not.toContain('author=');
+ });
+ });
+
+ describe('extractLubimyczytacId', () => {
+ it('extracts the numeric id from a book url', () => {
+ expect(extractLubimyczytacId('https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie')).toBe('123456');
+ expect(extractLubimyczytacId('/ksiazka/789012')).toBe('789012');
+ });
+
+ it('accepts a bare numeric id', () => {
+ expect(extractLubimyczytacId('4815162342')).toBe('4815162342');
+ });
+
+ it('returns undefined for non-book urls', () => {
+ expect(extractLubimyczytacId('https://lubimyczytac.pl/autor/123/foo')).toBeUndefined();
+ });
+ });
+
+ describe('extractLubimyczytacPath', () => {
+ it('keeps the id and slug as the canonical path', () => {
+ expect(extractLubimyczytacPath('https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie')).toBe('123456/ostatnie-zyczenie');
+ expect(extractLubimyczytacPath('/ksiazka/789012/x?ref=1')).toBe('789012/x');
+ expect(extractLubimyczytacPath('123456/ostatnie-zyczenie')).toBe('123456/ostatnie-zyczenie');
+ });
+
+ it('returns the bare id when no slug is present', () => {
+ expect(extractLubimyczytacPath('/ksiazka/789012')).toBe('789012');
+ });
+ });
+
+ describe('buildLubimyczytacBookUrl', () => {
+ it('builds a url from an id/slug path', () => {
+ expect(buildLubimyczytacBookUrl('123456/ostatnie-zyczenie')).toBe('https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie');
+ });
+
+ it('appends a placeholder slug for a bare numeric id (slug-less urls 404)', () => {
+ expect(buildLubimyczytacBookUrl('123456')).toBe('https://lubimyczytac.pl/ksiazka/123456/-');
+ });
+ });
+
+ describe('extractLubimyczytacSearchResults', () => {
+ it('extracts unique book links from search results', () => {
+ const html = `
+
+
+ `;
+ const results = extractLubimyczytacSearchResults(html, 5);
+ expect(results).toEqual([
+ { providerId: '123456/ostatnie-zyczenie', url: 'https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie' },
+ { providerId: '789012/miecz-przeznaczenia', url: 'https://lubimyczytac.pl/ksiazka/789012/miecz-przeznaczenia' },
+ ]);
+ });
+
+ it('falls back to the legacy authorAllBooks layout', () => {
+ const html = `
+
+ `;
+ expect(extractLubimyczytacSearchResults(html, 5)).toEqual([{ providerId: '555/legacy', url: 'https://lubimyczytac.pl/ksiazka/555/legacy' }]);
+ });
+
+ it('respects the limit', () => {
+ const html = `
+ A
+ B
+ `;
+ expect(extractLubimyczytacSearchResults(html, 1)).toHaveLength(1);
+ });
+ });
+
+ describe('parseLubimyczytacBookPage', () => {
+ it('parses a complete book page', () => {
+ const data = parseLubimyczytacBookPage(BOOK_HTML, 'https://lubimyczytac.pl/ksiazka/123456/ostatnie-zyczenie');
+ expect(data.providerId).toBe('123456/ostatnie-zyczenie');
+ expect(data.title).toBe('Ostatnie życzenie');
+ expect(data.authors).toEqual(['Andrzej Sapkowski']);
+ expect(data.description).toBe('Opis książki o wiedźminie Geralcie.');
+ expect(data.publisher).toBe('superNOWA');
+ expect(data.language).toBe('pl');
+ expect(data.pageCount).toBe(330);
+ expect(data.publishedYear).toBe(2014);
+ expect(data.isbn13).toBe('9788375780635');
+ expect(data.seriesName).toBe('Wiedźmin');
+ expect(data.seriesIndex).toBe(1);
+ expect(data.genres).toEqual(['Fantasy', 'fantastyka']);
+ expect(data.coverUrl).toBe('https://lubimyczytac.pl/upload/books/cover.jpg');
+ });
+
+ it('falls back to the json-ld genre slug when no tags are present', () => {
+ const html = `
+ Bez tagów
+
+ `;
+ expect(parseLubimyczytacBookPage(html).genres).toEqual(['poradniki']);
+ });
+
+ it('parses the series index from a trailing volume number', () => {
+ const html = `
+ Uwikłana
+ Cykl: Komisarz Tymon Hanter 4
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.seriesName).toBe('Komisarz Tymon Hanter');
+ expect(data.seriesIndex).toBe(4);
+ });
+
+ it('returns no title for an empty page', () => {
+ expect(parseLubimyczytacBookPage('').title).toBeUndefined();
+ });
+
+ it('derives providerId from canonical url in json-ld', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).providerId).toBe('999/canonical-slug');
+ });
+
+ it('derives providerId from sourceUrl when no canonical url in json-ld', () => {
+ const html = `Test `;
+ expect(parseLubimyczytacBookPage(html, 'https://lubimyczytac.pl/ksiazka/777/from-source').providerId).toBe('777/from-source');
+ });
+
+ it('returns undefined providerId when called without sourceUrl and no json-ld url', () => {
+ const html = `Test `;
+ expect(parseLubimyczytacBookPage(html).providerId).toBeUndefined();
+ });
+
+ it('handles invalid JSON in ld+json script gracefully', () => {
+ const html = `
+ Test
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.title).toBe('Test');
+ expect(data.pageCount).toBeUndefined();
+ });
+
+ it('parses numberOfPages from a string value in json-ld', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBe(330);
+ });
+
+ it('returns undefined pageCount for a non-numeric string in json-ld', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBeUndefined();
+ });
+
+ it('returns undefined pageCount when numberOfPages is an object', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBeUndefined();
+ });
+
+ it('returns undefined publishedYear when datePublished is a number', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).publishedYear).toBeUndefined();
+ });
+
+ it('returns undefined publishedYear when datePublished string contains no year', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).publishedYear).toBeUndefined();
+ });
+
+ it('extracts isbn10', () => {
+ const html = `
+ Test
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.isbn10).toBe('0743273567');
+ expect(data.isbn13).toBeUndefined();
+ });
+
+ it('uses og:image as cover fallback', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).coverUrl).toBe('https://example.com/cover.jpg');
+ });
+
+ it('resolves a protocol-relative cover url', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).coverUrl).toBe('https://static.lubimyczytac.pl/cover.jpg');
+ });
+
+ it('uses og:description as description fallback', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).description).toBe('Opis z og.');
+ });
+
+ it('passes through unknown language values unchanged', () => {
+ const html = `
+ Test
+ Język: klingon
+ `;
+ expect(parseLubimyczytacBookPage(html).language).toBe('klingon');
+ });
+
+ it('uses inLanguage from json-ld when no dt/dd language block', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).language).toBe('pl');
+ });
+
+ it('parses a plain string author from json-ld', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).authors).toEqual(['Andrzej Sapkowski']);
+ });
+
+ it('ignores author object with a non-string name', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).authors).toEqual([]);
+ });
+
+ it('ignores author object with a whitespace-only name', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).authors).toEqual([]);
+ });
+
+ it('ignores a whitespace-only string author', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).authors).toEqual([]);
+ });
+
+ it('returns undefined pageCount when numberOfPages overflows to Infinity', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBeUndefined();
+ });
+
+ it('parses json-ld wrapped in a @graph node', () => {
+ const html = `
+ Test
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.pageCount).toBe(200);
+ expect(data.publishedYear).toBe(2020);
+ });
+
+ it('parses json-ld from an array at root', () => {
+ const html = `
+ Test
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBe(100);
+ });
+
+ it('uses second json-ld script block when first has no book node', () => {
+ const html = `
+ Test
+
+
+ `;
+ expect(parseLubimyczytacBookPage(html).pageCount).toBe(150);
+ });
+
+ it('extracts genres from user tag links', () => {
+ const html = `
+ Test
+ Fantasy
+ Kryminał
+ `;
+ expect(parseLubimyczytacBookPage(html).genres).toEqual(['Fantasy', 'Kryminał']);
+ });
+
+ it('returns no series info when no cykl block is present', () => {
+ const data = parseLubimyczytacBookPage('Test ');
+ expect(data.seriesName).toBeUndefined();
+ expect(data.seriesIndex).toBeUndefined();
+ });
+
+ it('parses a decimal series index with comma notation', () => {
+ const html = `
+ Test
+ Cykl: Wiedźmin (tom 2,5)
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.seriesName).toBe('Wiedźmin');
+ expect(data.seriesIndex).toBe(2.5);
+ });
+
+ it('returns series name only when no volume number is present', () => {
+ const html = `
+ Test
+ Cykl: Wiedźmin
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.seriesName).toBe('Wiedźmin');
+ expect(data.seriesIndex).toBeUndefined();
+ });
+
+ it('deduplicates authors from html', () => {
+ const html = `
+ Test
+ Andrzej Sapkowski
+ Andrzej Sapkowski
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.authors?.filter((a) => a === 'Andrzej Sapkowski').length).toBe(1);
+ });
+
+ it('returns no metadata from a json-ld array containing no book nodes', () => {
+ const html = `
+ Test
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.pageCount).toBeUndefined();
+ expect(data.publishedYear).toBeUndefined();
+ });
+
+ it('parses an Audiobook type json-ld node', () => {
+ const html = `
+ Test Audio
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.pageCount).toBe(400);
+ expect(data.publishedYear).toBe(2021);
+ });
+
+ it('does not overwrite fields already parsed from an earlier json-ld block', () => {
+ const html = `
+ Test
+
+
+ `;
+ const data = parseLubimyczytacBookPage(html);
+ expect(data.pageCount).toBe(100);
+ expect(data.publishedYear).toBe(2019);
+ });
+
+ it('returns undefined for genres when no genre links or json-ld genre are present', () => {
+ const data = parseLubimyczytacBookPage('Test ');
+ expect(data.genres).toEqual([]);
+ });
+ });
+});
diff --git a/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.ts b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.ts
new file mode 100644
index 000000000..bee66ce6d
--- /dev/null
+++ b/server/src/modules/metadata-fetch/providers/lubimyczytac/lubimyczytac.scraper.ts
@@ -0,0 +1,345 @@
+import * as cheerio from 'cheerio';
+import type { CheerioAPI } from 'cheerio';
+
+export interface LubimyczytacSearchResultLink {
+ providerId: string;
+ url: string;
+}
+
+export interface LubimyczytacBookData {
+ providerId?: string;
+ title?: string;
+ authors?: string[];
+ description?: string;
+ publisher?: string;
+ publishedYear?: number;
+ language?: string;
+ pageCount?: number;
+ isbn10?: string;
+ isbn13?: string;
+ seriesName?: string;
+ seriesIndex?: number;
+ genres?: string[];
+ coverUrl?: string;
+ sourceUrl?: string;
+}
+
+const LUBIMYCZYTAC_BASE_URL = 'https://lubimyczytac.pl';
+const LUBIMYCZYTAC_SEARCH_PATH = '/szukaj/ksiazki';
+const BOOK_PATH_PATTERN = /\/ksiazka\/(\d+(?:\/[^/?#]+)?)/;
+const SERIES_INDEX_PATTERN = /\(tom\s+(\d+(?:[.,]\d+)?)\)/i;
+const MAX_GENRES = 10;
+
+const LANGUAGE_MAP: Record = {
+ polski: 'pl',
+ angielski: 'en',
+ niemiecki: 'de',
+ francuski: 'fr',
+ hiszpański: 'es',
+ włoski: 'it',
+};
+
+export function buildLubimyczytacSearchUrl(query: string, author?: string): string {
+ const url = new URL(LUBIMYCZYTAC_SEARCH_PATH, LUBIMYCZYTAC_BASE_URL);
+ url.searchParams.set('phrase', query);
+ if (author?.trim()) url.searchParams.set('author', author.trim());
+ return url.toString();
+}
+
+export function buildLubimyczytacBookUrl(providerId: string): string {
+ const path = extractLubimyczytacPath(providerId) ?? providerId.trim().replace(/^\/+|\/+$/g, '');
+ const segments = path.split('/').filter(Boolean);
+ // A bare numeric id (no slug) 404s; lubimyczytac.pl serves the book for any non-empty slug segment.
+ if (segments.length === 1) segments.push('-');
+ return `${LUBIMYCZYTAC_BASE_URL}/ksiazka/${segments.map(encodeURIComponent).join('/')}`;
+}
+
+// Canonical path after /ksiazka/, e.g. "123456/some-slug" (or just the numeric id when no slug is present).
+export function extractLubimyczytacPath(value: string): string | undefined {
+ const match = BOOK_PATH_PATTERN.exec(value);
+ if (match) return match[1];
+ const trimmed = value.trim().replace(/^\/+|\/+$/g, '');
+ return /^\d+(?:\/[^/?#]+)?$/.test(trimmed) ? trimmed : undefined;
+}
+
+// Numeric book id only - used for de-duplication and validation.
+export function extractLubimyczytacId(value: string): string | undefined {
+ const id = extractLubimyczytacPath(value)?.split('/')[0];
+ return id && /^\d+$/.test(id) ? id : undefined;
+}
+
+export function extractLubimyczytacSearchResults(html: string, limit: number): LubimyczytacSearchResultLink[] {
+ const $ = cheerio.load(html);
+ const results: LubimyczytacSearchResultLink[] = [];
+ const seen = new Set();
+
+ const anchors = [...$('a.book-card__title').toArray(), ...$('.authorAllBooks__single .authorAllBooks__singleTextTitle').toArray()];
+
+ for (const el of anchors) {
+ if (results.length >= limit) break;
+ const href = $(el).attr('href');
+ if (!href) continue;
+ const id = extractLubimyczytacId(href);
+ const providerId = extractLubimyczytacPath(href);
+ if (!id || !providerId || seen.has(id)) continue;
+ seen.add(id);
+ results.push({ providerId, url: resolveUrl(href) });
+ }
+
+ return results;
+}
+
+export function parseLubimyczytacBookPage(html: string, sourceUrl?: string): LubimyczytacBookData {
+ const $ = cheerio.load(html);
+ const jsonLd = parseJsonLd($);
+ const canonicalPath = jsonLd.url ? extractLubimyczytacPath(jsonLd.url) : undefined;
+ const sourcePath = sourceUrl ? extractLubimyczytacPath(sourceUrl) : undefined;
+ const { isbn10, isbn13 } = extractIsbn($);
+ const series = extractSeries($);
+
+ return {
+ providerId: canonicalPath ?? sourcePath,
+ title: extractTitle($),
+ authors: jsonLd.authors?.length ? jsonLd.authors : extractAuthors($),
+ description: extractDescription($),
+ publisher: extractPublisher($),
+ publishedYear: jsonLd.publishedYear,
+ language: extractLanguage($) ?? (jsonLd.language ? mapLanguage(jsonLd.language) : undefined),
+ pageCount: jsonLd.pageCount,
+ isbn10,
+ isbn13,
+ seriesName: series.name,
+ seriesIndex: series.index,
+ genres: extractGenres($, jsonLd.genre),
+ coverUrl: extractCoverUrl($),
+ sourceUrl,
+ };
+}
+
+function extractTitle($: CheerioAPI): string | undefined {
+ return cleanText($('h1.book__title').first().text()) || undefined;
+}
+
+function extractAuthors($: CheerioAPI): string[] {
+ const authors: string[] = [];
+ const seen = new Set();
+ $('a.link-name[href*="/autor/"], a[href*="/autor/"]').each((_, el) => {
+ const name = cleanText($(el).text());
+ if (!name || seen.has(name)) return;
+ seen.add(name);
+ authors.push(name);
+ });
+ return authors;
+}
+
+function extractDescription($: CheerioAPI): string | undefined {
+ // `.book__description` is the actual blurb; avoid `.collapse-content`, which the page also uses
+ // for user reviews and the ratings histogram.
+ return cleanText($('.book__description').first().text()) || cleanText($('meta[property="og:description"]').attr('content')) || undefined;
+}
+
+function extractPublisher($: CheerioAPI): string | undefined {
+ return cleanText($('a[href*="/wydawnictwo/"]').first().text()) || undefined;
+}
+
+function extractLanguage($: CheerioAPI): string | undefined {
+ let raw = '';
+ $('dt').each((_, el) => {
+ if (raw) return;
+ if (/język/i.test($(el).text())) raw = cleanText($(el).next('dd').text());
+ });
+ return raw ? mapLanguage(raw) : undefined;
+}
+
+function mapLanguage(raw: string): string {
+ const normalized = raw.trim().toLowerCase();
+ return LANGUAGE_MAP[normalized] ?? normalized;
+}
+
+function extractIsbn($: CheerioAPI): { isbn10?: string; isbn13?: string } {
+ const raw = cleanText($('meta[property="books:isbn"]').attr('content'));
+ const normalized = raw.replace(/[^0-9X]/gi, '').toUpperCase();
+ if (normalized.length === 13) return { isbn13: normalized };
+ if (normalized.length === 10) return { isbn10: normalized };
+ return {};
+}
+
+function extractSeries($: CheerioAPI): { name?: string; index?: number } {
+ let raw = '';
+ $('span.d-none.d-sm-block.mt-1').each((_, el) => {
+ if (raw) return;
+ const text = cleanText($(el).text());
+ if (text.startsWith('Cykl:')) raw = text;
+ });
+ if (!raw) return {};
+
+ const text = raw.slice('Cykl:'.length).trim();
+
+ // Preferred format: "Series Name (tom 4)"
+ const tomMatch = SERIES_INDEX_PATTERN.exec(text);
+ if (tomMatch) {
+ const index = parseFloat(tomMatch[1].replace(',', '.'));
+ return { name: text.replace(SERIES_INDEX_PATTERN, '').trim() || undefined, index: Number.isNaN(index) ? undefined : index };
+ }
+
+ // Fallback: "Series Name 4" (trailing standalone volume number)
+ const trailing = /\s(\d+(?:[.,]\d+)?)$/.exec(text);
+ if (trailing) {
+ const index = parseFloat(trailing[1].replace(',', '.'));
+ return { name: text.slice(0, trailing.index).trim() || undefined, index: Number.isNaN(index) ? undefined : index };
+ }
+
+ return { name: text || undefined };
+}
+
+function extractGenres($: CheerioAPI, jsonLdGenre?: string): string[] {
+ const genres: string[] = [];
+ const seen = new Set();
+ const add = (value: string | undefined) => {
+ const genre = cleanText(value);
+ if (!genre || seen.has(genre) || genres.length >= MAX_GENRES) return;
+ seen.add(genre);
+ genres.push(genre);
+ };
+
+ // Category breadcrumb, e.g. "Literatura obyczajowa, romans" -> split into individual genres
+ $('a.book__category').each((_, el) => {
+ for (const part of $(el).text().split(',')) add(part);
+ });
+ // User tags (older layout / when present)
+ $('a[href*="/ksiazki/t/"]').each((_, el) => add($(el).text()));
+ if (!genres.length && jsonLdGenre) add(deslugify(jsonLdGenre));
+ return genres;
+}
+
+function extractCoverUrl($: CheerioAPI): string | undefined {
+ const raw = $('.book-cover img').first().attr('src') || $('meta[property="og:image"]').attr('content');
+ if (!raw) return undefined;
+ return resolveUrl(raw);
+}
+
+type JsonLdFields = {
+ authors?: string[];
+ publishedYear?: number;
+ pageCount?: number;
+ genre?: string;
+ language?: string;
+ url?: string;
+};
+
+function parseJsonLd($: CheerioAPI): JsonLdFields {
+ const fields: JsonLdFields = {};
+
+ $('script[type="application/ld+json"]').each((_, el) => {
+ const node = findBookNode(safeJsonParse($(el).contents().text()));
+ if (!node) return;
+
+ if (!fields.authors) {
+ const authors = parseJsonLdAuthors(node.author);
+ if (authors.length) fields.authors = authors;
+ }
+ if (fields.pageCount === undefined) {
+ const pages = parseInteger(node.numberOfPages);
+ if (pages !== undefined) fields.pageCount = pages;
+ }
+ if (fields.publishedYear === undefined) {
+ const year = parseYear(node.datePublished);
+ if (year !== undefined) fields.publishedYear = year;
+ }
+ if (!fields.genre && typeof node.genre === 'string') {
+ fields.genre = node.genre.split('/').filter(Boolean).pop();
+ }
+ if (!fields.language && typeof node.inLanguage === 'string') {
+ fields.language = node.inLanguage;
+ }
+ if (!fields.url && typeof node.url === 'string') {
+ fields.url = node.url;
+ }
+ });
+
+ return fields;
+}
+
+type JsonLdNode = {
+ author?: unknown;
+ numberOfPages?: unknown;
+ datePublished?: unknown;
+ genre?: unknown;
+ inLanguage?: unknown;
+ url?: unknown;
+};
+
+function findBookNode(value: unknown): JsonLdNode | undefined {
+ if (Array.isArray(value)) {
+ for (const item of value) {
+ const found = findBookNode(item);
+ if (found) return found;
+ }
+ return undefined;
+ }
+ if (typeof value !== 'object' || value === null) return undefined;
+ const node = value as Record;
+ if ('@graph' in node) return findBookNode(node['@graph']);
+ const type = node['@type'];
+ if (type === 'Book' || type === 'Audiobook') return node as JsonLdNode;
+ if ('author' in node || 'numberOfPages' in node || 'datePublished' in node || 'genre' in node) return node as JsonLdNode;
+ return undefined;
+}
+
+function parseJsonLdAuthors(author: unknown): string[] {
+ const names: string[] = [];
+ const push = (entry: unknown) => {
+ if (typeof entry === 'string') {
+ const name = cleanText(entry);
+ if (name) names.push(name);
+ return;
+ }
+ if (typeof entry === 'object' && entry !== null && 'name' in entry) {
+ const rawName = (entry as { name?: unknown }).name;
+ if (typeof rawName === 'string') {
+ const name = cleanText(rawName);
+ if (name) names.push(name);
+ }
+ }
+ };
+
+ if (Array.isArray(author)) author.forEach(push);
+ else push(author);
+ return names;
+}
+
+function safeJsonParse(value: string): unknown {
+ try {
+ return JSON.parse(value);
+ } catch {
+ return undefined;
+ }
+}
+
+function resolveUrl(value: string): string {
+ if (value.startsWith('//')) return `https:${value}`;
+ return new URL(value, LUBIMYCZYTAC_BASE_URL).toString();
+}
+
+function cleanText(value: string | undefined): string {
+ return (value ?? '').replace(/\s+/g, ' ').trim();
+}
+
+function deslugify(value: string): string {
+ return value.replace(/[-_]+/g, ' ').trim();
+}
+
+function parseInteger(value: unknown): number | undefined {
+ if (typeof value === 'number') return Number.isFinite(value) ? Math.trunc(value) : undefined;
+ if (typeof value !== 'string') return undefined;
+ const match = value.replace(/,/g, '').match(/\d+/);
+ if (!match) return undefined;
+ const parsed = parseInt(match[0], 10);
+ return Number.isFinite(parsed) ? parsed : undefined;
+}
+
+function parseYear(value: unknown): number | undefined {
+ if (typeof value !== 'string') return undefined;
+ const match = value.match(/\b(1[0-9]{3}|2[0-9]{3})\b/);
+ return match ? parseInt(match[1], 10) : undefined;
+}
diff --git a/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts b/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts
index 3268caf18..9aa86ee4d 100644
--- a/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/open-library/open-library.provider.test.ts
@@ -19,6 +19,7 @@ describe('OpenLibraryProvider', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
beforeEach(() => {
diff --git a/server/src/modules/metadata-fetch/providers/provider-constants.ts b/server/src/modules/metadata-fetch/providers/provider-constants.ts
index dda1e1581..ad798a7e4 100644
--- a/server/src/modules/metadata-fetch/providers/provider-constants.ts
+++ b/server/src/modules/metadata-fetch/providers/provider-constants.ts
@@ -10,6 +10,7 @@ export const PROVIDER_LIMITS = {
COMICVINE_MAX_RESULTS: 10,
RANOBEDB_MAX_RESULTS: 5,
KOBO_MAX_RESULTS: 3,
+ LUBIMYCZYTAC_MAX_RESULTS: 5,
DEFAULT_SEARCH_RESULTS: 10,
} as const;
@@ -20,4 +21,5 @@ export const PROVIDER_DELAYS_MS = {
COMICVINE_VELOCITY_GUARD: 1_000,
RANOBEDB_BETWEEN_REQUESTS: 500,
KOBO_BETWEEN_REQUESTS: 700,
+ LUBIMYCZYTAC_BETWEEN_REQUESTS: 600,
} as const;
diff --git a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts
index cbe836a9c..8191b66ba 100644
--- a/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts
+++ b/server/src/modules/metadata-fetch/providers/ranobedb/ranobedb.provider.test.ts
@@ -19,6 +19,7 @@ const enabledConfig: ProviderConfigurations = {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: true },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
const disabledConfig: ProviderConfigurations = {
diff --git a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts
index c07437529..e371e8e3e 100644
--- a/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts
+++ b/server/src/modules/metadata-preferences/dto/update-provider-config.dto.ts
@@ -55,4 +55,5 @@ export class UpdateProviderConfigDto {
@IsOptional() @ValidateNested() @Type(() => ComicVineProviderConfigDto) comicvine?: ComicVineProviderConfigDto;
@IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) ranobedb?: SimpleProviderConfigDto;
@IsOptional() @ValidateNested() @Type(() => KoboProviderConfigDto) kobo?: KoboProviderConfigDto;
+ @IsOptional() @ValidateNested() @Type(() => SimpleProviderConfigDto) lubimyczytac?: SimpleProviderConfigDto;
}
diff --git a/server/src/modules/metadata-preferences/provider-config.service.test.ts b/server/src/modules/metadata-preferences/provider-config.service.test.ts
index 8535f8d3c..643c08530 100644
--- a/server/src/modules/metadata-preferences/provider-config.service.test.ts
+++ b/server/src/modules/metadata-preferences/provider-config.service.test.ts
@@ -74,6 +74,7 @@ describe('ProviderConfigService', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
});
});
@@ -356,6 +357,7 @@ describe('ProviderConfigService', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
});
expect(statuses.map((s) => s.key)).toEqual([
@@ -370,6 +372,7 @@ describe('ProviderConfigService', () => {
MetadataProviderKey.COMICVINE,
MetadataProviderKey.RANOBEDB,
MetadataProviderKey.KOBO,
+ MetadataProviderKey.LUBIMYCZYTAC,
]);
expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.configured).toBe(false);
expect(statuses.find((s) => s.key === MetadataProviderKey.GOOGLE)?.hint).toContain('API key required');
@@ -394,6 +397,7 @@ describe('ProviderConfigService', () => {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
});
expect(statuses.find((s) => s.key === MetadataProviderKey.HARDCOVER)?.configured).toBe(true);
diff --git a/server/src/modules/metadata-preferences/provider-config.service.ts b/server/src/modules/metadata-preferences/provider-config.service.ts
index 02498cae0..e784d6d8f 100644
--- a/server/src/modules/metadata-preferences/provider-config.service.ts
+++ b/server/src/modules/metadata-preferences/provider-config.service.ts
@@ -44,6 +44,7 @@ const DEFAULT_CONFIG: ProviderConfigurations = {
comicvine: { enabled: false, apiKey: '' },
ranobedb: { enabled: false },
kobo: { enabled: false, country: 'us', language: 'en' },
+ lubimyczytac: { enabled: false },
};
function asObject(value: unknown): Record {
@@ -137,6 +138,7 @@ const PROVIDER_LABELS: Record = {
[MetadataProviderKey.COMICVINE]: 'ComicVine',
[MetadataProviderKey.RANOBEDB]: 'RanobeDB',
[MetadataProviderKey.KOBO]: 'Kobo',
+ [MetadataProviderKey.LUBIMYCZYTAC]: 'LubimyCzytac',
};
type ProviderEnableRule = {
@@ -182,6 +184,7 @@ export class ProviderConfigService {
comicvine: { ...DEFAULT_CONFIG.comicvine },
ranobedb: { ...DEFAULT_CONFIG.ranobedb },
kobo: { ...DEFAULT_CONFIG.kobo },
+ lubimyczytac: { ...DEFAULT_CONFIG.lubimyczytac },
};
}
@@ -199,6 +202,7 @@ export class ProviderConfigService {
comicvine: mergeComicVineConfig(base.comicvine, next.comicvine),
ranobedb: mergeSimpleConfig(base.ranobedb, next.ranobedb),
kobo: mergeKoboConfig(base.kobo, next.kobo),
+ lubimyczytac: mergeSimpleConfig(base.lubimyczytac, next.lubimyczytac),
};
}
@@ -426,6 +430,13 @@ export class ProviderConfigService {
configured: true,
hint: 'Web scraping may be blocked by Kobo bot protection',
},
+ {
+ key: MetadataProviderKey.LUBIMYCZYTAC,
+ label: PROVIDER_LABELS[MetadataProviderKey.LUBIMYCZYTAC],
+ enabled: cfg.lubimyczytac.enabled,
+ configured: true,
+ hint: 'Polish book catalog (lubimyczytac.pl). Scrapes public pages.',
+ },
];
}
diff --git a/server/src/modules/metadata/extractors/comic-format.extractor.ts b/server/src/modules/metadata/extractors/comic-format.extractor.ts
index 244a7baae..a1a05089f 100644
--- a/server/src/modules/metadata/extractors/comic-format.extractor.ts
+++ b/server/src/modules/metadata/extractors/comic-format.extractor.ts
@@ -48,6 +48,7 @@ export class ComicFormatExtractor implements FormatExtractor {
openLibraryId: comicMetadata?.openLibraryId ?? null,
ranobedbId: comicMetadata?.ranobedbId ?? null,
koboId: comicMetadata?.koboId ?? null,
+ lubimyczytacId: comicMetadata?.lubimyczytacId ?? null,
itunesId: comicMetadata?.itunesId ?? null,
cover: cover ?? null,
comicMetadata: comicMetadata?.comicMetadata ?? null,
diff --git a/server/src/modules/metadata/extractors/format-extractor.interface.ts b/server/src/modules/metadata/extractors/format-extractor.interface.ts
index 3fe165347..ca6d887af 100644
--- a/server/src/modules/metadata/extractors/format-extractor.interface.ts
+++ b/server/src/modules/metadata/extractors/format-extractor.interface.ts
@@ -22,6 +22,7 @@ export interface ParsedBookData {
openLibraryId?: string | null;
ranobedbId?: string | null;
koboId?: string | null;
+ lubimyczytacId?: string | null;
itunesId?: string | null;
audibleId?: string | null;
cover: Buffer | null;
diff --git a/server/src/modules/metadata/extractors/opf-metadata.mapper.ts b/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
index 5f7c5ddc8..2d00021df 100644
--- a/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
+++ b/server/src/modules/metadata/extractors/opf-metadata.mapper.ts
@@ -25,6 +25,7 @@ export function hasOpfMetadata(metadata: ParsedOpf): boolean {
metadata.openLibraryId !== null ||
metadata.ranobedbId !== null ||
metadata.koboId !== null ||
+ metadata.lubimyczytacId !== null ||
metadata.itunesId !== null
);
}
@@ -53,6 +54,7 @@ export function mapOpfMetadata(metadata: ParsedOpf, cover: Buffer | null): Parse
openLibraryId: metadata.openLibraryId,
ranobedbId: metadata.ranobedbId,
koboId: metadata.koboId,
+ lubimyczytacId: metadata.lubimyczytacId,
itunesId: metadata.itunesId,
cover,
};
diff --git a/server/src/modules/metadata/extractors/pdf-format.extractor.ts b/server/src/modules/metadata/extractors/pdf-format.extractor.ts
index 57d9b97c9..5ecbdb006 100644
--- a/server/src/modules/metadata/extractors/pdf-format.extractor.ts
+++ b/server/src/modules/metadata/extractors/pdf-format.extractor.ts
@@ -37,6 +37,7 @@ export class PdfFormatExtractor implements FormatExtractor {
openLibraryId: pdf.openLibraryId,
ranobedbId: pdf.ranobedbId,
koboId: pdf.koboId,
+ lubimyczytacId: pdf.lubimyczytacId,
itunesId: pdf.itunesId,
cover: pdf.coverBuffer,
pageCount: pdf.pageCount,
diff --git a/server/src/modules/metadata/lib/cbz-metadata.ts b/server/src/modules/metadata/lib/cbz-metadata.ts
index d24dc82f3..a32e3b2c1 100644
--- a/server/src/modules/metadata/lib/cbz-metadata.ts
+++ b/server/src/modules/metadata/lib/cbz-metadata.ts
@@ -46,6 +46,7 @@ export interface ParsedCbzMetadata {
openLibraryId: string | null;
ranobedbId: string | null;
koboId: string | null;
+ lubimyczytacId: string | null;
itunesId: string | null;
comicMetadata: ParsedCbzComicMetadata | null;
}
@@ -267,6 +268,7 @@ function parseComicInfoXml(xmlBuf: Buffer): ParsedCbzMetadata | null {
openLibraryId: managedNotes.get('openLibraryId') ?? providerIdsFromWeb.openLibraryId ?? null,
ranobedbId: managedNotes.get('ranobedbId') ?? null,
koboId: managedNotes.get('koboId') ?? providerIdsFromWeb.koboId ?? null,
+ lubimyczytacId: managedNotes.get('lubimyczytacId') ?? null,
itunesId: null,
comicMetadata: hasComicFields
? {
@@ -324,6 +326,7 @@ function parseComicBookInfoJson(comment: string): ParsedCbzMetadata | null {
openLibraryId: null,
ranobedbId: null,
koboId: null,
+ lubimyczytacId: null,
itunesId: null,
comicMetadata: null,
};
diff --git a/server/src/modules/metadata/lib/opf-parser.ts b/server/src/modules/metadata/lib/opf-parser.ts
index ec43965b2..6fb40ff23 100644
--- a/server/src/modules/metadata/lib/opf-parser.ts
+++ b/server/src/modules/metadata/lib/opf-parser.ts
@@ -23,6 +23,7 @@ export interface ParsedOpf {
openLibraryId: string | null;
ranobedbId: string | null;
koboId: string | null;
+ lubimyczytacId: string | null;
itunesId: string | null;
coverHref: string | null;
}
@@ -208,6 +209,7 @@ export function parseOpf(xml: string): ParsedOpf {
let schemeOpenLibraryId: string | null = null;
let schemeRanobedbId: string | null = null;
let schemeKoboId: string | null = null;
+ let schemeLubimyczytacId: string | null = null;
let schemeItunesId: string | null = null;
let urnGoogleBooksId: string | null = null;
@@ -217,6 +219,7 @@ export function parseOpf(xml: string): ParsedOpf {
let urnOpenLibraryId: string | null = null;
let urnRanobedbId: string | null = null;
let urnKoboId: string | null = null;
+ let urnLubimyczytacId: string | null = null;
let urnItunesId: string | null = null;
for (const ident of toArray(metadata['identifier'])) {
@@ -240,6 +243,7 @@ export function parseOpf(xml: string): ParsedOpf {
if (scheme === 'openlibrary') schemeOpenLibraryId ??= value || null;
if (scheme === 'ranobedb') schemeRanobedbId ??= value || null;
if (scheme === 'kobo') schemeKoboId ??= value || null;
+ if (scheme === 'lubimyczytac') schemeLubimyczytacId ??= value || null;
if (scheme === 'itunes') schemeItunesId ??= value || null;
// urn:-prefixed provider identifiers (legacy / backward-compat)
@@ -250,6 +254,7 @@ export function parseOpf(xml: string): ParsedOpf {
if (value.startsWith('urn:openlibrary:')) urnOpenLibraryId ??= value.slice('urn:openlibrary:'.length) || null;
if (value.startsWith('urn:ranobedb:')) urnRanobedbId ??= value.slice('urn:ranobedb:'.length) || null;
if (value.startsWith('urn:kobo:')) urnKoboId ??= value.slice('urn:kobo:'.length) || null;
+ if (value.startsWith('urn:lubimyczytac:')) urnLubimyczytacId ??= value.slice('urn:lubimyczytac:'.length) || null;
if (value.startsWith('urn:itunes:')) urnItunesId ??= value.slice('urn:itunes:'.length) || null;
}
@@ -261,6 +266,7 @@ export function parseOpf(xml: string): ParsedOpf {
const openLibraryId = schemeOpenLibraryId ?? urnOpenLibraryId;
const ranobedbId = schemeRanobedbId ?? urnRanobedbId;
const koboId = schemeKoboId ?? urnKoboId;
+ const lubimyczytacId = schemeLubimyczytacId ?? urnLubimyczytacId;
const itunesId = schemeItunesId ?? urnItunesId;
isbn10 ??= propertyMeta('bookorbit:isbn10') ?? namedMeta('bookorbit:isbn10');
@@ -381,6 +387,7 @@ export function parseOpf(xml: string): ParsedOpf {
openLibraryId,
ranobedbId,
koboId,
+ lubimyczytacId,
itunesId,
coverHref,
};
diff --git a/server/src/modules/metadata/lib/pdf-parser.ts b/server/src/modules/metadata/lib/pdf-parser.ts
index 843b85ce8..9142fbd0c 100644
--- a/server/src/modules/metadata/lib/pdf-parser.ts
+++ b/server/src/modules/metadata/lib/pdf-parser.ts
@@ -29,6 +29,7 @@ export interface PdfParsed {
openLibraryId: string | null;
ranobedbId: string | null;
koboId: string | null;
+ lubimyczytacId: string | null;
itunesId: string | null;
coverBuffer: Buffer | null;
}
@@ -140,6 +141,7 @@ export async function parsePdfFile(absolutePath: string, options: PdfParseOption
openLibraryId: xmp?.openLibraryId ?? null,
ranobedbId: xmp?.ranobedbId ?? null,
koboId: xmp?.koboId ?? null,
+ lubimyczytacId: xmp?.lubimyczytacId ?? null,
itunesId: xmp?.itunesId ?? null,
coverBuffer,
};
diff --git a/server/src/modules/metadata/lib/pdf-xmp-reader.ts b/server/src/modules/metadata/lib/pdf-xmp-reader.ts
index 2eac0e642..f8b1585b5 100644
--- a/server/src/modules/metadata/lib/pdf-xmp-reader.ts
+++ b/server/src/modules/metadata/lib/pdf-xmp-reader.ts
@@ -26,6 +26,7 @@ export interface XmpParsed {
openLibraryId: string | null;
ranobedbId: string | null;
koboId: string | null;
+ lubimyczytacId: string | null;
itunesId: string | null;
}
@@ -159,6 +160,7 @@ export function parseXmp(xmpXml: string): XmpParsed | null {
openLibraryId: str(merged[`${px}:openLibraryId`]),
ranobedbId: str(merged[`${px}:ranobedbId`]),
koboId: str(merged[`${px}:koboId`]),
+ lubimyczytacId: str(merged[`${px}:lubimyczytacId`]),
itunesId: str(merged[`${px}:itunesId`]),
};
}
diff --git a/server/src/modules/metadata/metadata.service.ts b/server/src/modules/metadata/metadata.service.ts
index cd3910ca1..60b9beda0 100644
--- a/server/src/modules/metadata/metadata.service.ts
+++ b/server/src/modules/metadata/metadata.service.ts
@@ -578,6 +578,7 @@ export class MetadataService {
openLibraryId: data.openLibraryId,
ranobedbId: data.ranobedbId,
koboId: data.koboId,
+ lubimyczytacId: data.lubimyczytacId,
itunesId: data.itunesId,
comicMetadata: data.comicMetadata ?? undefined,
});
@@ -602,6 +603,7 @@ export class MetadataService {
if (filtered.openLibraryId !== undefined) scalarFields.openLibraryId = filtered.openLibraryId;
if (filtered.ranobedbId !== undefined) scalarFields.ranobedbId = filtered.ranobedbId;
if (filtered.koboId !== undefined) scalarFields.koboId = filtered.koboId;
+ if (filtered.lubimyczytacId !== undefined) scalarFields.lubimyczytacId = filtered.lubimyczytacId;
if (filtered.itunesId !== undefined) scalarFields.itunesId = filtered.itunesId;
if (Object.keys(scalarFields).length > 0) {
scalarFields.updatedAt = new Date();
diff --git a/server/src/modules/statistics/statistics.repository.ts b/server/src/modules/statistics/statistics.repository.ts
index 02749a0dd..7ac7b1c74 100644
--- a/server/src/modules/statistics/statistics.repository.ts
+++ b/server/src/modules/statistics/statistics.repository.ts
@@ -737,7 +737,8 @@ export class StatisticsRepository {
${bookMetadata.audibleId} is not null or
${bookMetadata.koboId} is not null or
${bookMetadata.comicvineId} is not null or
- ${bookMetadata.ranobedbId} is not null
+ ${bookMetadata.ranobedbId} is not null or
+ ${bookMetadata.lubimyczytacId} is not null
)`;
const [row] = await this.db