404 lines
12 KiB
Python
404 lines
12 KiB
Python
import re
|
|
|
|
from db import direct_connect
|
|
|
|
_DEFAULT_REGEX = [
|
|
r"^\s*[\*\-]{3,}\s*$",
|
|
r"^\s*[·•◦‣⁃]\s*[·•◦‣⁃]\s*[·•◦‣⁃]\s*$",
|
|
r"^\s*~{2,}\s*$",
|
|
r"^\s*={3,}\s*$",
|
|
r"^\s*#{3,}\s*$",
|
|
r"^\s*[oO0]{1,3}\s*$",
|
|
r"^\s*[-–—]\s*[oO0]\s*[-–—]\s*$",
|
|
r"^\s*[<>]+\s*[·•*]\s*[<>]+\s*$",
|
|
]
|
|
_DEFAULT_CSS = [
|
|
"hr",
|
|
"separator",
|
|
"section-break",
|
|
"divider",
|
|
"break",
|
|
"chapterbreak",
|
|
"scene-break",
|
|
"scenebreak",
|
|
]
|
|
|
|
|
|
def _exec(sql: str) -> None:
|
|
conn = direct_connect()
|
|
try:
|
|
with conn:
|
|
with conn.cursor() as cur:
|
|
cur.execute(sql)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def migrate_create_library() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS library (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) UNIQUE NOT NULL,
|
|
media_type VARCHAR(10) NOT NULL DEFAULT 'epub',
|
|
title VARCHAR(500),
|
|
author VARCHAR(255),
|
|
publisher VARCHAR(255),
|
|
series VARCHAR(500),
|
|
series_index INTEGER DEFAULT 0,
|
|
publication_status VARCHAR(100),
|
|
has_cover BOOLEAN DEFAULT FALSE,
|
|
description TEXT DEFAULT '',
|
|
source_url VARCHAR(1000),
|
|
publish_date DATE,
|
|
archived BOOLEAN DEFAULT FALSE,
|
|
want_to_read BOOLEAN DEFAULT FALSE,
|
|
needs_review BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_book_tags() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS book_tags (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) NOT NULL REFERENCES library(filename) ON DELETE CASCADE,
|
|
tag VARCHAR(255) NOT NULL,
|
|
tag_type VARCHAR(20) NOT NULL,
|
|
UNIQUE (filename, tag, tag_type)
|
|
)
|
|
"""
|
|
)
|
|
_exec("CREATE INDEX IF NOT EXISTS idx_book_tags_filename ON book_tags (filename)")
|
|
|
|
|
|
def migrate_create_reading_progress() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS reading_progress (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) UNIQUE NOT NULL REFERENCES library(filename) ON DELETE CASCADE,
|
|
cfi TEXT,
|
|
page INTEGER,
|
|
progress INTEGER DEFAULT 0,
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_reading_sessions() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS reading_sessions (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) NOT NULL REFERENCES library(filename) ON DELETE CASCADE,
|
|
read_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
_exec("CREATE INDEX IF NOT EXISTS idx_reading_sessions_filename ON reading_sessions (filename)")
|
|
|
|
|
|
def migrate_create_library_cover_cache() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS library_cover_cache (
|
|
filename VARCHAR(600) PRIMARY KEY REFERENCES library(filename) ON DELETE CASCADE,
|
|
mime_type VARCHAR(100) NOT NULL,
|
|
thumb_webp BYTEA NOT NULL,
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_credentials() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS credentials (
|
|
id SERIAL PRIMARY KEY,
|
|
site VARCHAR(255) UNIQUE NOT NULL,
|
|
username TEXT NOT NULL,
|
|
password TEXT NOT NULL,
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
_exec("ALTER TABLE credentials ALTER COLUMN username TYPE TEXT")
|
|
_exec("ALTER TABLE credentials ALTER COLUMN password TYPE TEXT")
|
|
|
|
|
|
def migrate_create_break_patterns() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS break_patterns (
|
|
id SERIAL PRIMARY KEY,
|
|
pattern_type VARCHAR(20) NOT NULL,
|
|
pattern TEXT NOT NULL,
|
|
enabled BOOLEAN DEFAULT TRUE,
|
|
is_default BOOLEAN DEFAULT FALSE,
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
UNIQUE (pattern_type, pattern)
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_seed_break_patterns() -> None:
|
|
conn = direct_connect()
|
|
try:
|
|
with conn:
|
|
with conn.cursor() as cur:
|
|
for pat in _DEFAULT_REGEX:
|
|
re.compile(pat)
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO break_patterns (pattern_type, pattern, is_default)
|
|
VALUES ('regex', %s, TRUE)
|
|
ON CONFLICT (pattern_type, pattern) DO NOTHING
|
|
""",
|
|
(pat,),
|
|
)
|
|
for pat in _DEFAULT_CSS:
|
|
cur.execute(
|
|
"""
|
|
INSERT INTO break_patterns (pattern_type, pattern, is_default)
|
|
VALUES ('css_class', %s, TRUE)
|
|
ON CONFLICT (pattern_type, pattern) DO NOTHING
|
|
""",
|
|
(pat,),
|
|
)
|
|
finally:
|
|
conn.close()
|
|
|
|
|
|
def migrate_create_backup_log() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS backup_log (
|
|
id SERIAL PRIMARY KEY,
|
|
status VARCHAR(20) NOT NULL,
|
|
files_count INTEGER,
|
|
size_bytes BIGINT,
|
|
error_msg TEXT,
|
|
started_at TIMESTAMP DEFAULT NOW(),
|
|
finished_at TIMESTAMP
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_add_rating() -> None:
|
|
_exec("ALTER TABLE library ADD COLUMN IF NOT EXISTS rating SMALLINT NOT NULL DEFAULT 0")
|
|
|
|
|
|
def migrate_create_bookmarks() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS bookmarks (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) NOT NULL REFERENCES library(filename) ON DELETE CASCADE,
|
|
chapter_index INTEGER NOT NULL DEFAULT 0,
|
|
scroll_frac REAL NOT NULL DEFAULT 0,
|
|
chapter_title VARCHAR(500) NOT NULL DEFAULT '',
|
|
note TEXT NOT NULL DEFAULT '',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
_exec("CREATE INDEX IF NOT EXISTS idx_bookmarks_filename ON bookmarks (filename)")
|
|
|
|
|
|
def migrate_remove_cover_missing_tag() -> None:
|
|
_exec("DELETE FROM book_tags WHERE tag = 'Cover Missing' AND tag_type = 'tag'")
|
|
|
|
|
|
def migrate_create_perf_indexes() -> None:
|
|
# Match library list sorting and common filters.
|
|
_exec(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_library_sort_coalesce
|
|
ON library (
|
|
(COALESCE(publisher, '')),
|
|
(COALESCE(author, '')),
|
|
(COALESCE(series, '')),
|
|
series_index,
|
|
(COALESCE(title, ''))
|
|
)
|
|
"""
|
|
)
|
|
_exec("CREATE INDEX IF NOT EXISTS idx_library_needs_review ON library (needs_review)")
|
|
_exec("CREATE INDEX IF NOT EXISTS idx_library_archived ON library (archived)")
|
|
|
|
# Speeds grouped reads + recent-read lookups.
|
|
_exec(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_reading_sessions_filename_readat
|
|
ON reading_sessions (filename, read_at DESC)
|
|
"""
|
|
)
|
|
|
|
# Helps ORDER BY filename, tag fetch for tag-map construction.
|
|
_exec(
|
|
"""
|
|
CREATE INDEX IF NOT EXISTS idx_book_tags_filename_tag
|
|
ON book_tags (filename, tag)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_series_suffix() -> None:
|
|
_exec(
|
|
"""
|
|
ALTER TABLE library
|
|
ADD COLUMN IF NOT EXISTS series_suffix VARCHAR(10) NOT NULL DEFAULT ''
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_builder_drafts() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS builder_drafts (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
title VARCHAR(500) NOT NULL,
|
|
author VARCHAR(255) NOT NULL,
|
|
publisher VARCHAR(255) NOT NULL DEFAULT '',
|
|
source_url VARCHAR(1000) NOT NULL DEFAULT '',
|
|
chapters JSONB NOT NULL DEFAULT '[]',
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_authors() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS authors (
|
|
id SERIAL PRIMARY KEY,
|
|
name VARCHAR(255) UNIQUE NOT NULL,
|
|
url VARCHAR(1000),
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_rename_hiatus() -> None:
|
|
_exec("UPDATE library SET publication_status = 'Long-Term Hold' WHERE publication_status = 'Hiatus'")
|
|
|
|
|
|
def migrate_add_storage_type() -> None:
|
|
_exec(
|
|
"ALTER TABLE library ADD COLUMN IF NOT EXISTS storage_type VARCHAR(10) NOT NULL DEFAULT 'file'"
|
|
)
|
|
|
|
|
|
def migrate_create_book_images() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS book_images (
|
|
sha256 CHAR(64) PRIMARY KEY,
|
|
ext VARCHAR(10) NOT NULL,
|
|
media_type VARCHAR(100) NOT NULL,
|
|
size_bytes INTEGER NOT NULL DEFAULT 0
|
|
)
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_book_chapters() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS book_chapters (
|
|
id SERIAL PRIMARY KEY,
|
|
filename VARCHAR(600) NOT NULL REFERENCES library(filename) ON DELETE CASCADE,
|
|
chapter_index INTEGER NOT NULL,
|
|
title VARCHAR(500) NOT NULL DEFAULT '',
|
|
content TEXT NOT NULL DEFAULT '',
|
|
content_tsv TSVECTOR,
|
|
UNIQUE (filename, chapter_index)
|
|
)
|
|
"""
|
|
)
|
|
_exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_book_chapters_filename ON book_chapters (filename, chapter_index)"
|
|
)
|
|
_exec(
|
|
"CREATE INDEX IF NOT EXISTS idx_book_chapters_tsv ON book_chapters USING GIN (content_tsv)"
|
|
)
|
|
|
|
|
|
def migrate_rebuild_chapter_tsv_with_title() -> None:
|
|
"""Rebuild content_tsv to include chapter title (safe to run repeatedly)."""
|
|
_exec(
|
|
"""
|
|
UPDATE book_chapters
|
|
SET content_tsv = to_tsvector('simple',
|
|
COALESCE(title, '') || ' ' ||
|
|
regexp_replace(COALESCE(content, ''), '<[^>]*>', ' ', 'g'))
|
|
"""
|
|
)
|
|
|
|
|
|
def migrate_create_app_settings() -> None:
|
|
_exec(
|
|
"""
|
|
CREATE TABLE IF NOT EXISTS app_settings (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
develop_mode BOOLEAN NOT NULL DEFAULT FALSE,
|
|
CONSTRAINT single_row CHECK (id = 1)
|
|
)
|
|
"""
|
|
)
|
|
_exec("INSERT INTO app_settings (id, develop_mode) VALUES (1, FALSE) ON CONFLICT DO NOTHING")
|
|
|
|
|
|
def migrate_app_settings_break_image() -> None:
|
|
_exec("ALTER TABLE app_settings ADD COLUMN IF NOT EXISTS break_image_sha256 VARCHAR(64) DEFAULT NULL")
|
|
_exec("ALTER TABLE app_settings ADD COLUMN IF NOT EXISTS break_image_ext VARCHAR(10) DEFAULT NULL")
|
|
|
|
|
|
def migrate_series_volume() -> None:
|
|
_exec(
|
|
"""
|
|
ALTER TABLE library
|
|
ADD COLUMN IF NOT EXISTS series_volume VARCHAR(20) NOT NULL DEFAULT ''
|
|
"""
|
|
)
|
|
|
|
|
|
def run_migrations() -> None:
|
|
migrate_create_library()
|
|
migrate_create_book_tags()
|
|
migrate_create_reading_progress()
|
|
migrate_create_reading_sessions()
|
|
migrate_create_library_cover_cache()
|
|
migrate_create_credentials()
|
|
migrate_create_break_patterns()
|
|
migrate_create_backup_log()
|
|
migrate_create_perf_indexes()
|
|
migrate_seed_break_patterns()
|
|
migrate_add_rating()
|
|
migrate_remove_cover_missing_tag()
|
|
migrate_create_bookmarks()
|
|
migrate_series_suffix()
|
|
migrate_create_builder_drafts()
|
|
migrate_create_authors()
|
|
migrate_rename_hiatus()
|
|
migrate_add_storage_type()
|
|
migrate_create_book_images()
|
|
migrate_create_book_chapters()
|
|
migrate_rebuild_chapter_tsv_with_title()
|
|
migrate_create_app_settings()
|
|
migrate_app_settings_break_image()
|
|
migrate_series_volume()
|