feat: persistent SQLite storage for bookmarks + positions keyed by content_hash

- new bookish/services/database.py: SQLite with WAL mode, tables for
  bookmarks and reading_positions, keyed by content_hash (SHA256)
- new bookish/routes/api.py: REST endpoints for CRUD bookmarks
  and save/load reading position
- upload endpoint returns content_hash (SHA256 of file)
- reader route computes SHA256 and passes BOOKISH_HASH to template
- reader.js: bookmarks + positions use API server as primary storage
  with localStorage fallback for offline/resilience
- position saves debounced (1.5s) to avoid hammering server
- .gitignore: added bookish.db
This commit is contained in:
truetype committed 2026-07-23 22:35:04 +02:00
1 parent 13e02faa8b
commit c1421665c0
8 files changed
+319 -25

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
import sqlite3
import os
import time
from flask import current_app
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..", "bookish.db")
def _get_db():
db = sqlite3.connect(DB_PATH)
db.row_factory = sqlite3.Row
db.execute("PRAGMA journal_mode=WAL")
db.execute("PRAGMA foreign_keys=ON")
return db
def init_db():
db = _get_db()
db.executescript("""
CREATE TABLE IF NOT EXISTS bookmarks (
id TEXT PRIMARY KEY,
content_hash TEXT NOT NULL,
type TEXT NOT NULL,
cfi TEXT,
href TEXT,
page INTEGER,
total_pages INTEGER,
scroll_pct INTEGER,
label TEXT NOT NULL,
chapter_title TEXT,
created REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_bookmarks_hash ON bookmarks(content_hash);
CREATE TABLE IF NOT EXISTS reading_positions (
content_hash TEXT PRIMARY KEY,
type TEXT NOT NULL,
cfi TEXT,
href TEXT,
page INTEGER,
scroll_pct INTEGER,
chapter_title TEXT,
updated REAL NOT NULL
);
""")
db.commit()
db.close()
# --- Bookmarks ---
def save_bookmark(data):
db = _get_db()
db.execute(
"""INSERT OR REPLACE INTO bookmarks
(id, content_hash, type, cfi, href, page, total_pages, scroll_pct, label, chapter_title, created)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
data["id"],
data["content_hash"],
data["type"],
data.get("cfi"),
data.get("href"),
data.get("page"),
data.get("total_pages"),
data.get("scroll_pct"),
data["label"],
data.get("chapter_title"),
data.get("created", time.time()),
),
)
db.commit()
db.close()
def get_bookmarks(content_hash):
db = _get_db()
rows = db.execute(
"SELECT * FROM bookmarks WHERE content_hash = ? ORDER BY created ASC",
(content_hash,),
).fetchall()
db.close()
return [dict(r) for r in rows]
def delete_bookmark(bookmark_id):
db = _get_db()
db.execute("DELETE FROM bookmarks WHERE id = ?", (bookmark_id,))
db.commit()
db.close()
# --- Reading positions ---
def save_position(content_hash, data):
db = _get_db()
db.execute(
"""INSERT OR REPLACE INTO reading_positions
(content_hash, type, cfi, href, page, scroll_pct, chapter_title, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
(
content_hash,
data["type"],
data.get("cfi"),
data.get("href"),
data.get("page"),
data.get("scroll_pct"),
data.get("chapter_title"),
time.time(),
),
)
db.commit()
db.close()
def get_position(content_hash):
db = _get_db()
row = db.execute(
"SELECT * FROM reading_positions WHERE content_hash = ?", (content_hash,)
).fetchone()
db.close()
return dict(row) if row else None