mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
cb9f1b81bc
Introduce full support for Audible series exploration: API routes, frontend pages, components, hooks, and integrations. Key changes: - Prisma: add Audiobook.seriesAsin for linking audiobooks to series detail pages. - Backend: add /api/series/search and /api/series/[asin] routes that require auth; scrape Audible series data and enrich books with library availability. - Integrations/services: add audible-series integration and update request/HTTP services to support the workflow. - Frontend: add /series and /series/[asin] pages, new components (SeriesCard, SeriesGrid, SeriesDetailCard, SimilarSeriesRow) and wire them to a new useSeries hook; update AudiobookDetailsModal to show/link series; add Series link to Header. - Misc: extend audiobook types with series fields and add seriesLabels to language-config for scraping. These changes enable users to search for series, view series metadata and books, and navigate between audiobook and series detail pages.
532 lines
22 KiB
Plaintext
532 lines
22 KiB
Plaintext
/**
|
|
* ReadMeABook Database Schema
|
|
* Documentation: documentation/backend/database.md
|
|
* ARCHITECTURE:
|
|
* - audible_cache: Pure Audible metadata (popular/new releases from Audible.com)
|
|
* - plex_library: Pure Plex library content (what's in your Plex server)
|
|
* - audiobooks: User-requested audiobooks only (created on request)
|
|
* - Matching happens at QUERY TIME by comparing audible_cache against plex_library
|
|
*/
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
output = "../src/generated/prisma"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "postgresql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
// ============================================================================
|
|
// MODELS
|
|
// ============================================================================
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
plexId String @unique @map("plex_id")
|
|
plexUsername String @map("plex_username")
|
|
plexEmail String? @map("plex_email")
|
|
role String @default("user") // 'user' or 'admin'
|
|
isSetupAdmin Boolean @default(false) @map("is_setup_admin") // First admin created during setup, cannot be demoted
|
|
avatarUrl String? @map("avatar_url")
|
|
authToken String? @map("auth_token") // Encrypted
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
lastLoginAt DateTime? @map("last_login_at")
|
|
|
|
// Plex Home profile tracking
|
|
plexHomeUserId String? @map("plex_home_user_id") // Profile ID from Plex Home (null = main account, set = home profile)
|
|
|
|
// Multi-auth support (for Audiobookshelf integration)
|
|
authProvider String? @map("auth_provider") // 'plex' | 'oidc' | 'local'
|
|
oidcSubject String? @map("oidc_subject") // OIDC subject ID (unique per provider)
|
|
oidcProvider String? @map("oidc_provider") // OIDC provider name (e.g., 'authentik', 'keycloak')
|
|
registrationStatus String? @default("approved") @map("registration_status") // 'pending_approval' | 'approved' | 'rejected'
|
|
|
|
// BookDate per-user preferences
|
|
bookDateLibraryScope String? @default("full") @map("bookdate_library_scope") // 'full' | 'rated' | 'favorites'
|
|
bookDateFavoriteBookIds String? @map("bookdate_favorite_book_ids") @db.Text // JSON array of PlexLibrary IDs (max 25)
|
|
bookDateCustomPrompt String? @map("bookdate_custom_prompt") @db.Text
|
|
bookDateOnboardingComplete Boolean @default(false) @map("bookdate_onboarding_complete")
|
|
|
|
// Request approval preferences
|
|
autoApproveRequests Boolean? @map("auto_approve_requests") // null = use global setting, true = auto-approve, false = require approval
|
|
|
|
// Fine-grained permissions
|
|
interactiveSearchAccess Boolean? @map("interactive_search_access") // null = use global setting, true = allow, false = deny
|
|
|
|
// Soft delete support
|
|
deletedAt DateTime? @map("deleted_at")
|
|
deletedBy String? @map("deleted_by") // Admin user ID who deleted this user
|
|
|
|
// Relations
|
|
requests Request[]
|
|
bookDateRecommendations BookDateRecommendation[]
|
|
bookDateSwipes BookDateSwipe[]
|
|
goodreadsShelves GoodreadsShelf[]
|
|
reportedIssues ReportedIssue[] @relation("Reporter")
|
|
resolvedIssues ReportedIssue[] @relation("Resolver")
|
|
|
|
@@index([plexId])
|
|
@@index([role])
|
|
@@index([deletedAt])
|
|
@@map("users")
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUDIBLE CACHE TABLE
|
|
// Pure Audible metadata - Popular/New Releases cached from Audible.com
|
|
// No Plex data, no availability status - just Audible metadata
|
|
// ============================================================================
|
|
model AudibleCache {
|
|
id String @id @default(uuid())
|
|
asin String @unique // Audible Standard Identification Number
|
|
title String
|
|
author String
|
|
narrator String?
|
|
description String? @db.Text
|
|
coverArtUrl String? @map("cover_art_url") @db.Text
|
|
cachedCoverPath String? @map("cached_cover_path") @db.Text // Local path to cached cover image
|
|
durationMinutes Int? @map("duration_minutes")
|
|
releaseDate DateTime? @map("release_date") @db.Date
|
|
rating Decimal? @db.Decimal(3, 2)
|
|
genres Json @default("[]")
|
|
|
|
// Discovery categories
|
|
isPopular Boolean @default(false) @map("is_popular")
|
|
isNewRelease Boolean @default(false) @map("is_new_release")
|
|
popularRank Int? @map("popular_rank")
|
|
newReleaseRank Int? @map("new_release_rank")
|
|
|
|
lastSyncedAt DateTime @default(now()) @map("last_synced_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([asin])
|
|
@@index([title])
|
|
@@index([author])
|
|
@@index([isPopular])
|
|
@@index([isNewRelease])
|
|
@@index([popularRank])
|
|
@@index([newReleaseRank])
|
|
@@map("audible_cache")
|
|
}
|
|
|
|
// ============================================================================
|
|
// LIBRARY CACHE TABLE (plex_library for backward compatibility)
|
|
// Universal library content - Works with Plex or Audiobookshelf backends
|
|
// Stores complete metadata including ASIN/ISBN for accurate matching
|
|
// No Audible data - just library backend metadata and file info
|
|
// ============================================================================
|
|
model PlexLibrary {
|
|
id String @id @default(uuid())
|
|
plexGuid String @unique @map("plex_guid") // Plex's unique identifier
|
|
plexRatingKey String? @map("plex_rating_key") // Plex's rating key
|
|
|
|
title String
|
|
author String
|
|
narrator String?
|
|
summary String? @db.Text
|
|
duration Int? // Duration in milliseconds (Plex format)
|
|
year Int?
|
|
userRating Decimal? @map("user_rating") @db.Decimal(3, 1) // User's rating (0-10 scale from Plex)
|
|
|
|
// Universal identifiers (works for both Plex and Audiobookshelf)
|
|
asin String? // Audible ASIN - extracted from Plex GUID or stored directly from ABS
|
|
isbn String? // ISBN (10 or 13) - for additional matching capability
|
|
|
|
// File information
|
|
filePath String? @map("file_path") @db.Text
|
|
thumbUrl String? @map("thumb_url") @db.Text // Plex thumbnail URL
|
|
cachedLibraryCoverPath String? @map("cached_library_cover_path") @db.Text // Local path to cached library cover image
|
|
|
|
// Plex metadata
|
|
plexLibraryId String @map("plex_library_id") // Which Plex library contains this
|
|
addedAt DateTime? @map("added_at") // When added to Plex
|
|
|
|
lastScannedAt DateTime @default(now()) @map("last_scanned_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([plexGuid])
|
|
@@index([title])
|
|
@@index([author])
|
|
@@index([plexLibraryId])
|
|
@@index([asin])
|
|
@@index([isbn])
|
|
@@map("plex_library")
|
|
}
|
|
|
|
// ============================================================================
|
|
// AUDIOBOOK TABLE (Simplified)
|
|
// Only created when user requests an audiobook
|
|
// Links to AudibleCache for metadata (optional - search results may not be cached)
|
|
// ============================================================================
|
|
model Audiobook {
|
|
id String @id @default(uuid())
|
|
|
|
// Core metadata (may come from Audible search, not necessarily cached)
|
|
audibleAsin String? @map("audible_asin") // ASIN if from Audible
|
|
title String
|
|
author String
|
|
narrator String?
|
|
description String? @db.Text
|
|
coverArtUrl String? @map("cover_art_url") @db.Text
|
|
year Int? // Release year extracted from releaseDate
|
|
series String? // Book series name (e.g., "The Mistborn Saga")
|
|
seriesPart String? @map("series_part") // Series position (e.g., "1", "1.5", "Book 1")
|
|
seriesAsin String? @map("series_asin") // Audible series ASIN for linking to series detail page
|
|
|
|
// Request tracking
|
|
status String @default("requested") // requested, downloading, processing, completed, failed
|
|
|
|
// File information (populated after download/organization)
|
|
filePath String? @map("file_path") @db.Text
|
|
fileFormat String? @map("file_format") // m4b, m4a, mp3
|
|
fileSizeBytes BigInt? @map("file_size_bytes")
|
|
|
|
// Plex integration (populated after successful import)
|
|
plexGuid String? @map("plex_guid") // Set when imported into Plex
|
|
plexLibraryId String? @map("plex_library_id")
|
|
|
|
// Audiobookshelf integration (alternative to Plex)
|
|
absItemId String? @map("abs_item_id") // Audiobookshelf item ID
|
|
|
|
// File hash for accurate library matching (SHA256 of sorted audio filenames)
|
|
filesHash String? @map("files_hash") @db.Text
|
|
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
// Relations
|
|
requests Request[]
|
|
reportedIssues ReportedIssue[]
|
|
|
|
@@index([audibleAsin])
|
|
@@index([plexGuid])
|
|
@@index([absItemId])
|
|
@@index([title])
|
|
@@index([author])
|
|
@@index([status])
|
|
@@index([filesHash])
|
|
@@map("audiobooks")
|
|
}
|
|
|
|
model Request {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
audiobookId String @map("audiobook_id")
|
|
status String @default("pending")
|
|
// Status values: pending, awaiting_approval, denied, searching, downloading, processing, downloaded, available, failed, cancelled, awaiting_search, awaiting_import, warn
|
|
// Flow (audiobook): pending → searching → downloading → processing → downloaded → available (when matched in Plex)
|
|
// Flow (ebook): pending → searching → downloading → processing → downloaded (terminal - no available state)
|
|
progress Int @default(0) // 0-100
|
|
priority Int @default(0)
|
|
errorMessage String? @map("error_message") @db.Text
|
|
selectedTorrent Json? @map("selected_torrent") // Pre-selected torrent from interactive search (stored when awaiting approval)
|
|
searchAttempts Int @default(0) @map("search_attempts")
|
|
downloadAttempts Int @default(0) @map("download_attempts")
|
|
importAttempts Int @default(0) @map("import_attempts")
|
|
maxImportRetries Int @default(5) @map("max_import_retries")
|
|
lastSearchAt DateTime? @map("last_search_at")
|
|
lastImportAt DateTime? @map("last_import_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
|
|
// Request type: 'audiobook' (default) or 'ebook'
|
|
// Ebook requests are created automatically when an audiobook is organized (if ebook downloads enabled)
|
|
type String @default("audiobook") // 'audiobook' | 'ebook'
|
|
parentRequestId String? @map("parent_request_id") // Links ebook request to originating audiobook request
|
|
|
|
// Soft delete support
|
|
deletedAt DateTime? @map("deleted_at")
|
|
deletedBy String? @map("deleted_by") // Admin user ID
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
audiobook Audiobook @relation(fields: [audiobookId], references: [id], onDelete: Cascade)
|
|
downloadHistory DownloadHistory[]
|
|
jobs Job[]
|
|
parentRequest Request? @relation("EbookParent", fields: [parentRequestId], references: [id], onDelete: SetNull)
|
|
childRequests Request[] @relation("EbookParent")
|
|
|
|
@@index([userId])
|
|
@@index([audiobookId])
|
|
@@index([status])
|
|
@@index([createdAt(sort: Desc)])
|
|
@@index([deletedAt])
|
|
@@index([type])
|
|
@@index([parentRequestId])
|
|
@@map("requests")
|
|
}
|
|
|
|
model DownloadHistory {
|
|
id String @id @default(uuid())
|
|
requestId String @map("request_id")
|
|
indexerName String @map("indexer_name")
|
|
indexerId Int? @map("indexer_id") // Prowlarr indexer ID for configuration lookup
|
|
torrentName String? @map("torrent_name")
|
|
torrentHash String? @map("torrent_hash")
|
|
nzbId String? @map("nzb_id") // SABnzbd NZB ID (mutually exclusive with torrentHash)
|
|
torrentSizeBytes BigInt? @map("torrent_size_bytes")
|
|
magnetLink String? @map("magnet_link") @db.Text
|
|
torrentUrl String? @map("torrent_url") @db.Text
|
|
seeders Int?
|
|
leechers Int?
|
|
qualityScore Int? @map("quality_score")
|
|
selected Boolean @default(false)
|
|
downloadClient String? @map("download_client") // qbittorrent, sabnzbd, direct (HTTP download for ebooks)
|
|
downloadClientId String? @map("download_client_id")
|
|
downloadStatus String? @map("download_status")
|
|
// Status values: queued, downloading, completed, failed, stalled
|
|
downloadError String? @map("download_error") @db.Text
|
|
downloadPath String? @map("download_path") @db.Text
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
request Request @relation(fields: [requestId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([requestId])
|
|
@@index([selected])
|
|
@@index([indexerId])
|
|
@@index([torrentHash])
|
|
@@index([nzbId])
|
|
@@index([createdAt(sort: Desc)])
|
|
@@map("download_history")
|
|
}
|
|
|
|
model Configuration {
|
|
id String @id @default(uuid())
|
|
key String @unique
|
|
value String? @db.Text
|
|
encrypted Boolean @default(false)
|
|
category String? // plex, indexer, download_client, system, automation
|
|
description String? @db.Text
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([key])
|
|
@@index([category])
|
|
@@map("configuration")
|
|
}
|
|
|
|
model Job {
|
|
id String @id @default(uuid())
|
|
bullJobId String? @map("bull_job_id")
|
|
requestId String? @map("request_id")
|
|
type String
|
|
// Job types: search_indexers, monitor_download, organize_files, scan_plex, plex_recently_added_check, match_plex
|
|
// Ebook job types: search_ebook, start_direct_download, monitor_direct_download
|
|
status String @default("pending")
|
|
// Status values: pending, active, completed, failed, delayed, stuck
|
|
priority Int @default(0)
|
|
attempts Int @default(0)
|
|
maxAttempts Int @default(3) @map("max_attempts")
|
|
payload Json?
|
|
result Json?
|
|
errorMessage String? @map("error_message") @db.Text
|
|
stackTrace String? @map("stack_trace") @db.Text
|
|
startedAt DateTime? @map("started_at")
|
|
completedAt DateTime? @map("completed_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
request Request? @relation(fields: [requestId], references: [id], onDelete: SetNull)
|
|
events JobEvent[]
|
|
|
|
@@index([requestId])
|
|
@@index([type])
|
|
@@index([status])
|
|
@@index([createdAt(sort: Desc)])
|
|
@@map("jobs")
|
|
}
|
|
|
|
model JobEvent {
|
|
id String @id @default(uuid())
|
|
jobId String @map("job_id")
|
|
level String // info, warn, error
|
|
context String // e.g., OrganizeFiles, FileOrganizer, MonitorDownload
|
|
message String @db.Text
|
|
metadata Json? // Additional structured data
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
job Job @relation(fields: [jobId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([jobId])
|
|
@@index([createdAt])
|
|
@@map("job_events")
|
|
}
|
|
|
|
model ScheduledJob {
|
|
id String @id @default(uuid())
|
|
name String
|
|
type String // 'plex_library_scan', 'plex_recently_added_check', 'audible_refresh', 'retry_missing_torrents', 'retry_failed_imports', 'cleanup_seeded_torrents', 'monitor_rss_feeds'
|
|
schedule String // Cron expression
|
|
enabled Boolean @default(true)
|
|
payload Json @default("{}")
|
|
lastRun DateTime? @map("last_run")
|
|
lastRunJobId String? @map("last_run_job_id") // Bull queue job ID of most recent execution
|
|
nextRun DateTime? @map("next_run")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([type])
|
|
@@index([enabled])
|
|
@@map("scheduled_jobs")
|
|
}
|
|
|
|
// ============================================================================
|
|
// BOOKDATE TABLES
|
|
// AI-powered audiobook recommendation system
|
|
// Documentation: documentation/features/bookdate-prd.md
|
|
// ============================================================================
|
|
|
|
model BookDateConfig {
|
|
id String @id @default(uuid())
|
|
provider String // 'openai' | 'claude' | 'custom'
|
|
apiKey String @map("api_key") @db.Text // Encrypted at rest (AES-256)
|
|
model String // e.g., 'gpt-4o', 'claude-sonnet-4-5-20250929'
|
|
baseUrl String? @map("base_url") @db.Text // Base URL for custom provider (OpenAI-compatible endpoints)
|
|
isVerified Boolean @default(false) @map("is_verified")
|
|
isEnabled Boolean @default(true) @map("is_enabled") // Admin toggle (global feature)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@map("bookdate_config")
|
|
}
|
|
|
|
// Note: BookDateConfig is now a singleton - only ONE record exists globally.
|
|
// Admin configures this in settings, and all users share the same API key.
|
|
// Individual users still have their own recommendations and swipe history.
|
|
|
|
model BookDateRecommendation {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
batchId String @map("batch_id") // Group recommendations from same AI call
|
|
title String
|
|
author String
|
|
narrator String?
|
|
rating Decimal? @db.Decimal(3, 2)
|
|
description String? @db.Text
|
|
coverUrl String? @map("cover_url") @db.Text
|
|
audnexusAsin String? @map("audnexus_asin") // For matching
|
|
aiReason String @map("ai_reason") @db.Text // Why AI recommended this
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
swipes BookDateSwipe[]
|
|
|
|
@@index([userId, batchId])
|
|
@@index([userId, createdAt])
|
|
@@map("bookdate_recommendations")
|
|
}
|
|
|
|
model BookDateSwipe {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
recommendationId String? @map("recommendation_id") // NULL if book not from BookDate
|
|
bookTitle String @map("book_title")
|
|
bookAuthor String @map("book_author")
|
|
action String // 'left' | 'right' | 'up'
|
|
markedAsKnown Boolean @default(false) @map("marked_as_known") // True if "Mark as Known"
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
recommendation BookDateRecommendation? @relation(fields: [recommendationId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId, createdAt])
|
|
@@index([recommendationId])
|
|
@@map("bookdate_swipes")
|
|
}
|
|
|
|
model NotificationBackend {
|
|
id String @id @default(uuid())
|
|
type String // 'discord' | 'pushover' | 'email' | 'slack' | 'telegram' | 'webhook'
|
|
name String // User-friendly label
|
|
config Json // Type-specific config (encrypted sensitive values)
|
|
events Json @default("[]") // Array of event strings
|
|
enabled Boolean @default(true)
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([enabled])
|
|
@@map("notification_backends")
|
|
}
|
|
|
|
// ============================================================================
|
|
// REPORTED ISSUES TABLE
|
|
// User-reported problems with available audiobooks (corrupted, wrong book, etc.)
|
|
// ============================================================================
|
|
|
|
model ReportedIssue {
|
|
id String @id @default(uuid())
|
|
audiobookId String @map("audiobook_id")
|
|
reporterId String @map("reporter_id")
|
|
reason String @db.VarChar(250)
|
|
status String @default("open") // open, dismissed, replaced
|
|
resolvedAt DateTime? @map("resolved_at")
|
|
resolvedById String? @map("resolved_by_id")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
audiobook Audiobook @relation(fields: [audiobookId], references: [id], onDelete: Cascade)
|
|
reporter User @relation("Reporter", fields: [reporterId], references: [id], onDelete: Cascade)
|
|
resolvedBy User? @relation("Resolver", fields: [resolvedById], references: [id], onDelete: SetNull)
|
|
|
|
@@index([audiobookId])
|
|
@@index([reporterId])
|
|
@@index([status])
|
|
@@map("reported_issues")
|
|
}
|
|
|
|
// ============================================================================
|
|
// GOODREADS SYNC TABLES
|
|
// Per-user Goodreads shelf subscriptions + global book-to-ASIN mapping cache
|
|
// ============================================================================
|
|
|
|
model GoodreadsShelf {
|
|
id String @id @default(uuid())
|
|
userId String @map("user_id")
|
|
name String // Extracted from RSS <title>
|
|
rssUrl String @map("rss_url") @db.Text
|
|
lastSyncAt DateTime? @map("last_sync_at")
|
|
bookCount Int? @map("book_count")
|
|
coverUrls String? @map("cover_urls") @db.Text // JSON array of cover image URLs
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
// Relations
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, rssUrl])
|
|
@@index([userId])
|
|
@@map("goodreads_shelves")
|
|
}
|
|
|
|
model GoodreadsBookMapping {
|
|
id String @id @default(uuid())
|
|
goodreadsBookId String @unique @map("goodreads_book_id")
|
|
title String
|
|
author String
|
|
audibleAsin String? @map("audible_asin")
|
|
coverUrl String? @map("cover_url") @db.Text
|
|
noMatch Boolean @default(false) @map("no_match")
|
|
lastSearchAt DateTime? @map("last_search_at")
|
|
createdAt DateTime @default(now()) @map("created_at")
|
|
updatedAt DateTime @updatedAt @map("updated_at")
|
|
|
|
@@index([goodreadsBookId])
|
|
@@index([audibleAsin])
|
|
@@map("goodreads_book_mappings")
|
|
}
|