books: schema, migration and ingest script (WIP)
This commit is contained in:
162
src/lib/server/db/schema/books.ts
Normal file
162
src/lib/server/db/schema/books.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
bigint,
|
||||
check,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
primaryKey,
|
||||
real,
|
||||
text,
|
||||
uniqueIndex
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import { booksSchema } from './schemas';
|
||||
import { id, timestamps, timestamptz } from './helpers';
|
||||
|
||||
// Book tracker + reader. A work is one book (any edition we care about); a work can
|
||||
// exist without files (placeholder for a book Stan wants but doesn't have yet).
|
||||
// Files live on stan-vm under $BOOKS_ROOT/library (target /srv/books/library); covers under $BOOKS_ROOT/covers.
|
||||
|
||||
export const bookMetadataStatus = booksSchema.enum('metadata_status', [
|
||||
'pending', // not looked up yet
|
||||
'matched', // confident match (ISBN or exact title+author)
|
||||
'low_confidence', // fuzzy match, needs a human look
|
||||
'not_found',
|
||||
'manual' // edited by hand; enrichment never overwrites it
|
||||
]);
|
||||
|
||||
export const bookWorks = booksSchema.table(
|
||||
'works',
|
||||
{
|
||||
id: id(),
|
||||
title: text('title').notNull(),
|
||||
subtitle: text('subtitle'),
|
||||
authors: text('authors').array().notNull().default([]),
|
||||
isbn10: text('isbn10'),
|
||||
isbn13: text('isbn13'),
|
||||
publisher: text('publisher'),
|
||||
year: integer('year'),
|
||||
language: text('language'), // ISO 639-1 where known, e.g. 'en', 'nl'
|
||||
pageCount: integer('page_count'),
|
||||
description: text('description'),
|
||||
tags: text('tags').array().notNull().default([]),
|
||||
coverUrl: text('cover_url'), // remote source of the cover
|
||||
coverPath: text('cover_path'), // local file, relative to /srv/books/covers
|
||||
// External ids
|
||||
openlibraryWorkId: text('openlibrary_work_id'), // e.g. OL45883W
|
||||
openlibraryEditionId: text('openlibrary_edition_id'), // e.g. OL7353617M
|
||||
googleBooksId: text('google_books_id'),
|
||||
annasMd5: text('annas_md5').array().notNull().default([]), // Anna's Archive / libgen file md5s
|
||||
sourceRefs: jsonb('source_refs').$type<Record<string, unknown>>().notNull().default({}),
|
||||
metadataStatus: bookMetadataStatus('metadata_status').notNull().default('pending'),
|
||||
metadataSource: text('metadata_source'), // 'openlibrary' | 'googlebooks' | 'libgen' | 'manual'
|
||||
metadataFetchedAt: timestamptz('metadata_fetched_at'),
|
||||
notes: text('notes'),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('works_isbn13_key')
|
||||
.on(t.isbn13)
|
||||
.where(sql`${t.isbn13} is not null`),
|
||||
index('works_title_idx').on(sql`lower(${t.title})`),
|
||||
check('works_year_check', sql`${t.year} is null or ${t.year} between 0 and 2100`)
|
||||
]
|
||||
);
|
||||
|
||||
export const bookFiles = booksSchema.table(
|
||||
'files',
|
||||
{
|
||||
id: id(),
|
||||
workId: integer('work_id')
|
||||
.notNull()
|
||||
.references(() => bookWorks.id, { onDelete: 'cascade' }),
|
||||
path: text('path').notNull(), // relative to /srv/books/library
|
||||
format: text('format').notNull(), // epub | pdf | mobi | azw3 | djvu
|
||||
sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
|
||||
sha256: text('sha256').notNull(),
|
||||
md5: text('md5'),
|
||||
source: text('source'), // 'gdrive' | 'textbooks-repo' | 'annas' | 'upload'
|
||||
...timestamps
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('files_path_key').on(t.path),
|
||||
uniqueIndex('files_sha256_key').on(t.sha256),
|
||||
index('files_work_id_idx').on(t.workId)
|
||||
]
|
||||
);
|
||||
|
||||
export const bookShelves = booksSchema.table(
|
||||
'shelves',
|
||||
{
|
||||
id: id(),
|
||||
slug: text('slug').notNull(),
|
||||
name: text('name').notNull(),
|
||||
sortOrder: integer('sort_order').notNull().default(0),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [uniqueIndex('shelves_slug_key').on(t.slug)]
|
||||
);
|
||||
|
||||
export const bookShelfWorks = booksSchema.table(
|
||||
'shelf_works',
|
||||
{
|
||||
shelfId: integer('shelf_id')
|
||||
.notNull()
|
||||
.references(() => bookShelves.id, { onDelete: 'cascade' }),
|
||||
workId: integer('work_id')
|
||||
.notNull()
|
||||
.references(() => bookWorks.id, { onDelete: 'cascade' }),
|
||||
sortOrder: integer('sort_order').notNull().default(0),
|
||||
addedAt: timestamptz('added_at').notNull().defaultNow()
|
||||
},
|
||||
(t) => [
|
||||
primaryKey({ columns: [t.shelfId, t.workId] }),
|
||||
index('shelf_works_work_id_idx').on(t.workId)
|
||||
]
|
||||
);
|
||||
|
||||
export const bookProgress = booksSchema.table(
|
||||
'progress',
|
||||
{
|
||||
id: id(),
|
||||
workId: integer('work_id')
|
||||
.notNull()
|
||||
.references(() => bookWorks.id, { onDelete: 'cascade' }),
|
||||
fileId: integer('file_id').references(() => bookFiles.id, { onDelete: 'set null' }),
|
||||
locator: text('locator'), // EPUB CFI or 'page:123' for PDFs
|
||||
page: integer('page'),
|
||||
percent: real('percent').notNull().default(0),
|
||||
startedAt: timestamptz('started_at'),
|
||||
finishedAt: timestamptz('finished_at'),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [
|
||||
uniqueIndex('progress_work_id_key').on(t.workId),
|
||||
check('progress_percent_check', sql`${t.percent} between 0 and 100`)
|
||||
]
|
||||
);
|
||||
|
||||
export const bookNotes = booksSchema.table(
|
||||
'notes',
|
||||
{
|
||||
id: id(),
|
||||
workId: integer('work_id')
|
||||
.notNull()
|
||||
.references(() => bookWorks.id, { onDelete: 'cascade' }),
|
||||
fileId: integer('file_id').references(() => bookFiles.id, { onDelete: 'set null' }),
|
||||
locator: text('locator'),
|
||||
page: integer('page'),
|
||||
highlight: text('highlight'), // the selected text, if any
|
||||
note: text('note'),
|
||||
color: text('color'),
|
||||
...timestamps
|
||||
},
|
||||
(t) => [index('notes_work_id_idx').on(t.workId)]
|
||||
);
|
||||
|
||||
export type BookWork = typeof bookWorks.$inferSelect;
|
||||
export type NewBookWork = typeof bookWorks.$inferInsert;
|
||||
export type BookFile = typeof bookFiles.$inferSelect;
|
||||
export type BookShelf = typeof bookShelves.$inferSelect;
|
||||
export type BookProgress = typeof bookProgress.$inferSelect;
|
||||
export type BookNote = typeof bookNotes.$inferSelect;
|
||||
Reference in New Issue
Block a user