import dataclasses import hashlib import os import sqlite3 from datetime import datetime from typing import List, Tuple from epy_reader.ebooks import Ebook from epy_reader.models import AppData, LibraryItem, Optional, ReadingState class State(AppData): """ Use sqlite3 instead of JSON (in older version) to shift the weight from memory to process """ def __init__(self): if not os.path.isfile(self.filepath): self.init_db() @property def filepath(self) -> str: return os.path.join(self.prefix, "states.db") if self.prefix else os.devnull def get_from_history(self) -> List[LibraryItem]: try: conn = sqlite3.connect(self.filepath) cur = conn.cursor() cur.execute( """ SELECT last_read, filepath, title, author, reading_progress FROM library ORDER BY last_read DESC """ ) results = cur.fetchall() library_items: List[LibraryItem] = [] for result in results: library_items.append( LibraryItem( last_read=datetime.fromisoformat(result[0]), filepath=result[1], title=result[2], author=result[3], reading_progress=result[4], ) ) return library_items finally: conn.close() def delete_from_library(self, filepath: str) -> None: try: conn = sqlite3.connect(self.filepath) conn.execute("PRAGMA foreign_keys = ON") conn.execute("DELETE FROM reading_states WHERE filepath=?", (filepath,)) conn.commit() finally: conn.close() def get_last_read(self) -> Optional[str]: library = self.get_from_history() return library[0].filepath if library else None def update_library(self, ebook: Ebook, reading_progress: Optional[float]) -> None: try: metadata = ebook.get_meta() conn = sqlite3.connect(self.filepath) conn.execute( """ INSERT OR REPLACE INTO library (filepath, title, author, reading_progress) VALUES (?, ?, ?, ?) """, (ebook.path, metadata.title, metadata.creator, reading_progress), ) conn.commit() finally: conn.close() def get_last_reading_state(self, ebook: Ebook) -> ReadingState: try: conn = sqlite3.connect(self.filepath) conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute("SELECT * FROM reading_states WHERE filepath=?", (ebook.path,)) result = cur.fetchone() if result: result = dict(result) del result["filepath"] return ReadingState(**result, section=None) return ReadingState(content_index=0, textwidth=80, row=0, rel_pctg=None, section=None) finally: conn.close() def set_last_reading_state(self, ebook: Ebook, reading_state: ReadingState) -> None: try: conn = sqlite3.connect(self.filepath) conn.execute( """ INSERT OR REPLACE INTO reading_states VALUES (:filepath, :content_index, :textwidth, :row, :rel_pctg) """, {"filepath": ebook.path, **dataclasses.asdict(reading_state)}, ) conn.commit() finally: conn.close() def insert_bookmark(self, ebook: Ebook, name: str, reading_state: ReadingState) -> None: try: conn = sqlite3.connect(self.filepath) conn.execute( """ INSERT INTO bookmarks VALUES (:id, :filepath, :name, :content_index, :textwidth, :row, :rel_pctg) """, { "id": hashlib.sha1(f"{ebook.path}{name}".encode()).hexdigest()[:10], "filepath": ebook.path, "name": name, **dataclasses.asdict(reading_state), }, ) conn.commit() finally: conn.close() def delete_bookmark(self, ebook: Ebook, name: str) -> None: try: conn = sqlite3.connect(self.filepath) conn.execute("DELETE FROM bookmarks WHERE filepath=? AND name=?", (ebook.path, name)) conn.commit() finally: conn.close() def get_bookmarks(self, ebook: Ebook) -> List[Tuple[str, ReadingState]]: try: conn = sqlite3.connect(self.filepath) conn.row_factory = sqlite3.Row cur = conn.cursor() cur.execute("SELECT * FROM bookmarks WHERE filepath=?", (ebook.path,)) results = cur.fetchall() bookmarks: List[Tuple[str, ReadingState]] = [] for result in results: tmp_dict = dict(result) name = tmp_dict["name"] tmp_dict = { k: v for k, v in tmp_dict.items() if k in ("content_index", "textwidth", "row", "rel_pctg") } bookmarks.append((name, ReadingState(**tmp_dict))) return bookmarks finally: conn.close() def init_db(self) -> None: try: conn = sqlite3.connect(self.filepath) conn.executescript( """ CREATE TABLE reading_states ( filepath TEXT PRIMARY KEY, content_index INTEGER, textwidth INTEGER, row INTEGER, rel_pctg REAL ); CREATE TABLE library ( last_read DATETIME DEFAULT (datetime('now','localtime')), filepath TEXT PRIMARY KEY, title TEXT, author TEXT, reading_progress REAL, FOREIGN KEY (filepath) REFERENCES reading_states(filepath) ON DELETE CASCADE ); CREATE TABLE bookmarks ( id TEXT PRIMARY KEY, filepath TEXT, name TEXT, content_index INTEGER, textwidth INTEGER, row INTEGER, rel_pctg REAL, FOREIGN KEY (filepath) REFERENCES reading_states(filepath) ON DELETE CASCADE ); """ ) conn.commit() finally: conn.close()