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:
1 parent
13e02faa8b
commit
c1421665c0
8 files changed
+319
-25
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user